diff --git a/backend/src/main/scala/bloop/Compiler.scala b/backend/src/main/scala/bloop/Compiler.scala index d9b836b365..f4e1ce4b07 100644 --- a/backend/src/main/scala/bloop/Compiler.scala +++ b/backend/src/main/scala/bloop/Compiler.scala @@ -74,7 +74,8 @@ case class CompileInputs( ioExecutor: Executor, invalidatedClassFilesInDependentProjects: Set[File], generatedClassFilePathsInDependentProjects: Map[String, File], - resources: List[AbsolutePath] + resources: List[AbsolutePath], + resourceMappings: List[(AbsolutePath, String)] ) case class CompileOutPaths( diff --git a/backend/src/main/scala/bloop/tracing/BraveTracer.scala b/backend/src/main/scala/bloop/tracing/BraveTracer.scala index a52f4288fd..cdfc764f3a 100644 --- a/backend/src/main/scala/bloop/tracing/BraveTracer.scala +++ b/backend/src/main/scala/bloop/tracing/BraveTracer.scala @@ -1,10 +1,13 @@ package bloop.tracing import java.util.concurrent.ConcurrentHashMap +import java.nio.file.{Paths => NioPaths, Files} +import java.net.URI import scala.util.control.NonFatal import bloop.task.Task +import bloop.io.AbsolutePath import brave.Span import brave.Tracer @@ -21,6 +24,8 @@ sealed trait BraveTracer { thunk: BraveTracer => T ): T + def tag(key: String, value: String): Unit + def traceVerbose[T](name: String, tags: (String, String)*)( thunk: BraveTracer => T ): T @@ -49,6 +54,8 @@ object NoopTracer extends BraveTracer { override def startNewChildTracer(name: String, tags: (String, String)*): BraveTracer = this + override def tag(key: String, value: String): Unit = () + override def trace[T](name: String, tags: (String, String)*)(thunk: BraveTracer => T): T = thunk( this ) @@ -91,9 +98,22 @@ object BraveTracer { if (properties.enabled) { BraveTracerInternal(name, properties, ctx, tags: _*) } else { - NoopTracer + val buildUri = tags.collectFirst { case ("workspace.dir", value) => value } + buildUri match { + case Some(uri) => + val workspaceDir = AbsolutePath(NioPaths.get(uri)) + val traceFile = workspaceDir.resolve("compilation-trace.json") + if (traceFile.exists) { + val projectName = tags + .collectFirst { case ("compile.target", value) => value } + .getOrElse(name) + new CompilationTraceTracer(projectName, traceFile, System.currentTimeMillis()) + } else { + NoopTracer + } + case None => NoopTracer + } } - } } @@ -104,6 +124,11 @@ final class BraveTracerInternal private ( properties: TraceProperties ) extends BraveTracer { + override def tag(key: String, value: String): Unit = { + _currentSpan.tag(key, value) + () + } + def currentSpan = Some(_currentSpan) def startNewChildTracer(name: String, tags: (String, String)*): BraveTracer = { @@ -263,3 +288,86 @@ object BraveTracerInternal { new BraveTracerInternal(tracer, rootSpan, closeEverything, properties) } } + +final class CompilationTraceTracer( + project: String, + traceFile: bloop.io.AbsolutePath, + startTime: Long +) extends BraveTracer { + import bloop.tracing.CompilationTrace + import bloop.tracing.TraceArtifacts + import com.github.plokhotnyuk.jsoniter_scala.core.writeToArray + import com.github.plokhotnyuk.jsoniter_scala.core.WriterConfig + + private val tags = new ConcurrentHashMap[String, String]() + + override def startNewChildTracer(name: String, tags: (String, String)*): BraveTracer = this + override def tag(key: String, value: String): Unit = { + this.tags.put(key, value) + () + } + + override def trace[T](name: String, tags: (String, String)*)(thunk: BraveTracer => T): T = + thunk(this) + override def traceVerbose[T](name: String, tags: (String, String)*)( + thunk: BraveTracer => T + ): T = thunk(this) + override def traceTask[T](name: String, tags: (String, String)*)( + thunk: BraveTracer => Task[T] + ): Task[T] = thunk(this) + override def traceTaskVerbose[T](name: String, tags: (String, String)*)( + thunk: BraveTracer => Task[T] + ): Task[T] = thunk(this) + + override def terminate(): Unit = { + val durationMs = System.currentTimeMillis() - startTime + val isNoOp = tags.getOrDefault("isNoOp", "false").toBoolean + val analysisOut = tags.getOrDefault("analysis", "") + val classesDir = tags.getOrDefault("classesDir", "") + val artifacts = TraceArtifacts(classesDir, analysisOut) + val fileCount = tags.getOrDefault("fileCount", "0").toInt + val files = (0 until fileCount).map(i => tags.get(s"file.$i")).filter(_ != null) + + import com.github.plokhotnyuk.jsoniter_scala.core.readFromString + val diagnosticCount = tags.getOrDefault("diagnostics.count", "0").toInt + val diagnostics = (0 until diagnosticCount).flatMap { i => + val json = tags.get(s"diagnostic.$i") + if (json != null) { + try { + Some(readFromString[TraceDiagnostic](json)(CompilationTrace.diagnosticCodec)) + } catch { case NonFatal(_) => None } + } else None + } + + val trace = CompilationTrace( + project, + files, + diagnostics, + artifacts, + isNoOp, + durationMs + ) + + try { + if (!java.nio.file.Files.exists(traceFile.getParent.underlying)) { + java.nio.file.Files.createDirectories(traceFile.getParent.underlying) + } + traceFile.getParent.underlying.synchronized { + val projectTraceFile = traceFile.getParent.resolve(s"compilation-trace-${project}.json") + val bytes = writeToArray(trace, WriterConfig.withIndentionStep(4))(CompilationTrace.codec) + java.nio.file.Files.write(projectTraceFile.underlying, bytes) + () + } + } catch { + case NonFatal(e) => e.printStackTrace() + } + } + + override def currentSpan: Option[Span] = None + override def toIndependentTracer( + name: String, + traceProperties: TraceProperties, + tags: (String, String)* + ): BraveTracer = + this +} diff --git a/backend/src/main/scala/bloop/tracing/CompilationTrace.scala b/backend/src/main/scala/bloop/tracing/CompilationTrace.scala new file mode 100644 index 0000000000..c83dadc412 --- /dev/null +++ b/backend/src/main/scala/bloop/tracing/CompilationTrace.scala @@ -0,0 +1,42 @@ +package bloop.tracing + +import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec +import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker + +case class CompilationTrace( + project: String, + files: Seq[String], + diagnostics: Seq[TraceDiagnostic], + artifacts: TraceArtifacts, + isNoOp: Boolean, + durationMs: Long +) + +object CompilationTrace { + implicit val codec: JsonValueCodec[CompilationTrace] = + JsonCodecMaker.make[CompilationTrace] + implicit val listCodec: JsonValueCodec[List[CompilationTrace]] = + JsonCodecMaker.make[List[CompilationTrace]] + implicit val diagnosticCodec: JsonValueCodec[TraceDiagnostic] = + JsonCodecMaker.make[TraceDiagnostic] +} + +case class TraceDiagnostic( + severity: String, + message: String, + range: Option[TraceRange], + code: Option[String], + source: Option[String] +) + +case class TraceRange( + startLine: Int, + startCharacter: Int, + endLine: Int, + endCharacter: Int +) + +case class TraceArtifacts( + classesDir: String, + analysisFile: String +) diff --git a/backend/src/main/scala/bloop/tracing/TraceProperties.scala b/backend/src/main/scala/bloop/tracing/TraceProperties.scala index 07d5cf3aea..8f4a7efcdd 100644 --- a/backend/src/main/scala/bloop/tracing/TraceProperties.scala +++ b/backend/src/main/scala/bloop/tracing/TraceProperties.scala @@ -9,7 +9,8 @@ case class TraceProperties( localServiceName: String, traceStartAnnotation: Option[String], traceEndAnnotation: Option[String], - enabled: Boolean + enabled: Boolean, + compilationTrace: Boolean ) object TraceProperties { @@ -20,6 +21,7 @@ object TraceProperties { val localServiceName = Properties.propOrElse("bloop.tracing.localServiceName", "bloop") val traceStartAnnotation = Properties.propOrNone("bloop.tracing.traceStartAnnotation") val traceEndAnnotation = Properties.propOrNone("bloop.tracing.traceEndAnnotation") + val compilationTrace = Properties.propOrFalse("bloop.tracing.compilationTrace") val traceServerUrl = Properties.propOrElse( "zipkin.server.url", @@ -33,7 +35,8 @@ object TraceProperties { localServiceName, traceStartAnnotation, traceEndAnnotation, - enabled + enabled, + compilationTrace ) } } diff --git a/frontend/src/it/scala/bloop/CommunityBuild.scala b/frontend/src/it/scala/bloop/CommunityBuild.scala index 1953f63fea..12f2d3eb98 100644 --- a/frontend/src/it/scala/bloop/CommunityBuild.scala +++ b/frontend/src/it/scala/bloop/CommunityBuild.scala @@ -130,6 +130,7 @@ abstract class CommunityBuild(val buildpressHomeDir: AbsolutePath) { scalaInstance = allProjectsInBuild.head.project.scalaInstance, rawClasspath = Nil, resources = Nil, + resourceMappings = Nil, compileSetup = Config.CompileSetup.empty, genericClassesDir = dummyClassesDir, isBestEffort = false, diff --git a/frontend/src/main/scala/bloop/data/Project.scala b/frontend/src/main/scala/bloop/data/Project.scala index daa6517014..4f85bb9e4f 100644 --- a/frontend/src/main/scala/bloop/data/Project.scala +++ b/frontend/src/main/scala/bloop/data/Project.scala @@ -39,6 +39,7 @@ final case class Project( scalaInstance: Option[ScalaInstance], rawClasspath: List[AbsolutePath], resources: List[AbsolutePath], + resourceMappings: List[(AbsolutePath, String)], compileSetup: Config.CompileSetup, genericClassesDir: AbsolutePath, isBestEffort: Boolean, @@ -347,6 +348,8 @@ object Project { val tags = project.tags.getOrElse(Nil) val projectDirectory = AbsolutePath(project.directory) + val resourceMappings = List.empty[(AbsolutePath, String)] + Project( project.name, projectDirectory, @@ -355,6 +358,7 @@ object Project { instance, compileClasspath, compileResources, + resourceMappings, setup, AbsolutePath(project.classesDir), isBestEffort = false, diff --git a/frontend/src/main/scala/bloop/data/TraceSettings.scala b/frontend/src/main/scala/bloop/data/TraceSettings.scala index 38a1ed3785..596cb00c2a 100644 --- a/frontend/src/main/scala/bloop/data/TraceSettings.scala +++ b/frontend/src/main/scala/bloop/data/TraceSettings.scala @@ -9,7 +9,8 @@ case class TraceSettings( localServiceName: Option[String], traceStartAnnotation: Option[String], traceEndAnnotation: Option[String], - enabled: Option[Boolean] + enabled: Option[Boolean], + compilationTrace: Option[Boolean] ) object TraceSettings { @@ -22,7 +23,8 @@ object TraceSettings { settings.localServiceName.getOrElse(default.localServiceName), settings.traceStartAnnotation.orElse(default.traceStartAnnotation), settings.traceEndAnnotation.orElse(default.traceEndAnnotation), - settings.enabled.getOrElse(default.enabled) + settings.enabled.getOrElse(default.enabled), + settings.compilationTrace.getOrElse(default.compilationTrace) ) } @@ -34,7 +36,8 @@ object TraceSettings { localServiceName = Some(properties.localServiceName), traceStartAnnotation = properties.traceStartAnnotation, traceEndAnnotation = properties.traceEndAnnotation, - enabled = Some(properties.enabled) + enabled = Some(properties.enabled), + compilationTrace = Some(properties.compilationTrace) ) } } diff --git a/frontend/src/main/scala/bloop/engine/caches/ResultsCache.scala b/frontend/src/main/scala/bloop/engine/caches/ResultsCache.scala index 85300de330..7bc00d197b 100644 --- a/frontend/src/main/scala/bloop/engine/caches/ResultsCache.scala +++ b/frontend/src/main/scala/bloop/engine/caches/ResultsCache.scala @@ -119,7 +119,7 @@ final case class ResultsCache private ( def addFinalResults(ps: List[FinalCompileResult]): ResultsCache = { ps.foldLeft(this) { - case (rs, FinalNormalCompileResult(p, r)) => rs.addResult(p, r) + case (rs, FinalNormalCompileResult(p, r, _)) => rs.addResult(p, r) case (rs, FinalEmptyResult) => rs } } diff --git a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala index dac5807179..26cdd7579e 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala @@ -40,6 +40,8 @@ import monix.reactive.MulticastStrategy import monix.reactive.Observable import xsbti.compile.CompileAnalysis import xsbti.compile.MiniSetup +import xsbti.compile.CompileAnalysis +import xsbti.compile.MiniSetup import xsbti.compile.PreviousResult import java.nio.file.Path @@ -74,6 +76,7 @@ object CompileTask { traceProperties, "bloop.version" -> BuildInfo.version, "zinc.version" -> BuildInfo.zincVersion, + "workspace.dir" -> cwd.syntax, "build.uri" -> originUri.syntax, "compile.target" -> topLevelTargets, "client" -> clientName @@ -84,6 +87,7 @@ object CompileTask { traceProperties, "bloop.version" -> BuildInfo.version, "zinc.version" -> BuildInfo.zincVersion, + "workspace.dir" -> cwd.syntax, "build.uri" -> originUri.syntax, "compile.target" -> topLevelTargets, "client" -> clientName @@ -123,12 +127,15 @@ object CompileTask { logger, ExecutionContext.ioScheduler ) + + // Also copy mapped resources if any + val allCopyTasks = copyResourcesTask Task.now( ResultBundle( Compiler.Result.Empty, None, None, - copyResourcesTask.runAsync(ExecutionContext.ioScheduler) + allCopyTasks.map(_ => ()).runAsync(ExecutionContext.ioScheduler) ) ) case Right(CompileSourcesAndInstance(sources, instance, _)) => @@ -184,7 +191,8 @@ object CompileTask { ExecutionContext.ioExecutor, bundle.dependenciesData.allInvalidatedClassFiles, bundle.dependenciesData.allGeneratedClassFilePaths, - project.runtimeResources + project.runtimeResources, + project.resourceMappings ) } @@ -245,8 +253,14 @@ object CompileTask { compileProjectTracer, logger ) - .doOnFinish(_ => Task(compileProjectTracer.terminate())) - postCompilationTasks.runAsync(ExecutionContext.ioScheduler) + + // Copy mapped resources after compilation + val allTasks = Task + .gatherUnordered(List(postCompilationTasks)) + .map(_ => ()) + .doOnFinish(_ => Task(compileProjectTracer.terminate())) + + allTasks.runAsync(ExecutionContext.ioScheduler) } // Populate the last successful result if result was success @@ -270,8 +284,36 @@ object CompileTask { // Memoize so that no matter how many times it's run, it's executed only once val newSuccessful = LastSuccessfulResult(bundle.uniqueInputs, s.products, populatingTask.memoize) + + // Tag tracer with success results + compileProjectTracer.tag("success", "true") + compileProjectTracer.tag("isNoOp", s.isNoOp.toString) + compileProjectTracer.tag("classesDir", s.products.newClassesDir.toString) + compileProjectTracer.tag("analysis", compileOut.analysisOut.toString) + + val sources = bundle.uniqueInputs.sources + compileProjectTracer.tag("fileCount", sources.length.toString) + sources.zipWithIndex.foreach { + case (file, idx) => + compileProjectTracer.tag(s"file.$idx", file.toPath.toString) + } + + reportDiagnostics(compileProjectTracer, s.reporter.allProblems) + ResultBundle(s, Some(newSuccessful), Some(lastSuccessful), runningTasks) case f: Compiler.Result.Failed => + compileProjectTracer.tag("success", "false") + + val sources = bundle.uniqueInputs.sources + compileProjectTracer.tag("fileCount", sources.length.toString) + sources.zipWithIndex.foreach { + case (file, idx) => + compileProjectTracer.tag(s"file.$idx", file.toPath.toString) + } + + val problems = f.problems.map(_.problem) + reportDiagnostics(compileProjectTracer, problems) + val runningTasks = runPostCompilationTasks(f.backgroundTasks) ResultBundle(result, None, Some(lastSuccessful), runningTasks) case c: Compiler.Result.Cancelled => @@ -347,7 +389,7 @@ object CompileTask { markUnusedClassesDirAndCollectCleanUpTasks(results, state, tracer, rawLogger) val failures = results.flatMap { - case FinalNormalCompileResult(p, results) => + case FinalNormalCompileResult(p, results, _) => results.fromCompiler match { case Compiler.Result.NotOk(_) => List(p) // Consider success with reported fatal warnings as error to simulate -Xfatal-warnings @@ -393,7 +435,7 @@ object CompileTask { val runningTasksRequiredForCorrectness = Task.sequence { results.flatMap { - case FinalNormalCompileResult(_, result) => + case FinalNormalCompileResult(_, result, _) => val tasksAtEndOfBuildCompilation = Task.fromFuture(result.runningBackgroundTasks) List(tasksAtEndOfBuildCompilation) @@ -459,7 +501,7 @@ object CompileTask { val compilerResult = resultBundle.fromCompiler val previousResult = finalResult match { - case FinalNormalCompileResult(p, _) => + case FinalNormalCompileResult(p, _, _) => previousState.results.all.get(p) case _ => None } @@ -590,4 +632,45 @@ object CompileTask { ) .map(_ => ()) } + + private def reportDiagnostics( + tracer: BraveTracer, + problems: Seq[xsbti.Problem] + ): Unit = { + import bloop.tracing.TraceDiagnostic + import bloop.tracing.TraceRange + import com.github.plokhotnyuk.jsoniter_scala.core.writeToString + + def toOption[T](opt: java.util.Optional[T]): Option[T] = + if (opt.isPresent) Some(opt.get) else None + + tracer.tag("diagnostics.count", problems.length.toString) + problems.zipWithIndex.foreach { + case (p, idx) => + val range = toOption(p.position.startLine).map { startLine => + TraceRange( + startLine.intValue(), + toOption(p.position.startColumn).map(_.intValue()).getOrElse(0), + toOption(p.position.endLine).map(_.intValue()).getOrElse(startLine.intValue()), + toOption(p.position.endColumn).map(_.intValue()).getOrElse(0) + ) + } + + val diagnostic = TraceDiagnostic( + p.severity.toString, + p.message, + range, + toOption(p.diagnosticCode).map(_.code), + toOption(p.position.sourcePath) + ) + + try { + val json = writeToString(diagnostic)(bloop.tracing.CompilationTrace.diagnosticCodec) + tracer.tag(s"diagnostic.$idx", json) + } catch { + case scala.util.control.NonFatal(e) => + tracer.tag(s"diagnostic.$idx.error", e.getMessage) + } + } + } } diff --git a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala index 86cc87f7f5..af16e6eccb 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala @@ -397,7 +397,7 @@ object CompileGraph { * `FailPromise` exception that makes the partial result be recognized as error. */ def toPartialFailure(bundle: SuccessfulCompileBundle, results: ResultBundle): PartialFailure = { - PartialFailure(bundle.project, FailedOrCancelledPromise, Task.now(results)) + PartialFailure(bundle.project, FailedOrCancelledPromise, Task.now(results), Some(bundle)) } def loop(dag: Dag[Project]): CompileTraversal = { @@ -437,7 +437,7 @@ object CompileGraph { val transitive = dagResults.flatMap(Dag.dfs(_, mode = Dag.PreOrder)).distinct transitive.flatMap { case PartialSuccess(bundle, result) => Some(result.map(r => bundle.project -> r)) - case PartialFailure(project, _, result) => Some(result.map(r => project -> r)) + case PartialFailure(project, _, result, _) => Some(result.map(r => project -> r)) case _ => None } } diff --git a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileResult.scala b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileResult.scala index 816b3c96d5..2fbb54c6a2 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileResult.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileResult.scala @@ -41,10 +41,10 @@ object PartialCompileResult { def toFinalResult(result: PartialCompileResult): Task[List[FinalCompileResult]] = { result match { case PartialEmpty => Task.now(FinalEmptyResult :: Nil) - case PartialFailure(project, _, bundle) => - bundle.map(b => FinalNormalCompileResult(project, b) :: Nil) + case PartialFailure(project, _, resultTask, bundleOpt) => + resultTask.map(b => FinalNormalCompileResult(project, b, bundleOpt) :: Nil) case PartialSuccess(bundle, result) => - result.map(res => FinalNormalCompileResult(bundle.project, res) :: Nil) + result.map(res => FinalNormalCompileResult(bundle.project, res, Some(bundle)) :: Nil) } } } @@ -57,7 +57,8 @@ case object PartialEmpty extends PartialCompileResult { case class PartialFailure( project: Project, exception: Throwable, - result: Task[ResultBundle] + result: Task[ResultBundle], + bundle: Option[SuccessfulCompileBundle] = None ) extends PartialCompileResult with CacheHashCode {} @@ -77,7 +78,8 @@ case object FinalEmptyResult extends FinalCompileResult { case class FinalNormalCompileResult private ( project: Project, - result: ResultBundle + result: ResultBundle, + bundle: Option[SuccessfulCompileBundle] ) extends FinalCompileResult with CacheHashCode @@ -102,7 +104,7 @@ object FinalCompileResult { override def shows(r: FinalCompileResult): String = { r match { case FinalEmptyResult => s" (product of dag aggregation)" - case FinalNormalCompileResult(project, result) => + case FinalNormalCompileResult(project, result, _) => val projectName = project.name result.fromCompiler match { case Compiler.Result.Empty => s"${projectName} (empty)" diff --git a/frontend/src/test/resources/source-generator.py b/frontend/src/test/resources/source-generator.py index e7042e3db8..bba834dabf 100644 --- a/frontend/src/test/resources/source-generator.py +++ b/frontend/src/test/resources/source-generator.py @@ -62,3 +62,7 @@ def main(output_dir, args): def random(): return 123 + +def random(): + return 123 + diff --git a/frontend/src/test/scala/bloop/BuildLoaderSpec.scala b/frontend/src/test/scala/bloop/BuildLoaderSpec.scala index f4f96cfa85..4b93d714b3 100644 --- a/frontend/src/test/scala/bloop/BuildLoaderSpec.scala +++ b/frontend/src/test/scala/bloop/BuildLoaderSpec.scala @@ -304,7 +304,8 @@ object BuildLoaderSpec extends BaseSuite { localServiceName = Some("42"), traceStartAnnotation = Some("start"), traceEndAnnotation = Some("end"), - enabled = Some(true) + enabled = Some(true), + compilationTrace = None ) ), None diff --git a/frontend/src/test/scala/bloop/CompilationTraceSpec.scala b/frontend/src/test/scala/bloop/CompilationTraceSpec.scala new file mode 100644 index 0000000000..6517aea056 --- /dev/null +++ b/frontend/src/test/scala/bloop/CompilationTraceSpec.scala @@ -0,0 +1,140 @@ +package bloop + +import bloop.util.TestUtil +import bloop.logging.RecordingLogger +import bloop.data.WorkspaceSettings +import bloop.data.TraceSettings +import bloop.tracing.CompilationTrace +import java.nio.file.Files +import bloop.cli.ExitStatus +import com.github.plokhotnyuk.jsoniter_scala.core.readFromArray + +object CompilationTraceSpec extends BaseCompileSpec { + override protected val TestProject = util.TestProject + + test("compilation trace is created when enabled") { + TestUtil.withinWorkspace { workspace => + val sources = List( + """/main/scala/Foo.scala + |class Foo + """.stripMargin + ) + + val logger = new RecordingLogger(ansiCodesSupported = false, debug = false) + val `A` = TestProject(workspace, "a", sources) + val projects = List(`A`) + + // Write workspace settings with compilationTrace enabled + val settings = WorkspaceSettings( + None, + None, + None, + None, + Some(TraceSettings(None, None, None, None, None, None, None, Some(true))), + None + ) + val configDir = workspace.resolve(".bloop") + if (!Files.exists(configDir.underlying)) Files.createDirectories(configDir.underlying) + WorkspaceSettings.writeToFile(configDir, settings, logger) + Files.createFile(workspace.resolve("compilation-trace.json").underlying) + + val state = loadState(workspace, projects, logger) + val compiledState = state.compile(`A`) + assertExitStatus(compiledState, ExitStatus.Ok) + + val traceFile = workspace.resolve("compilation-trace-a.json") + assert(Files.exists(traceFile.underlying)) + + val bytes = Files.readAllBytes(traceFile.underlying) + val trace = readFromArray[CompilationTrace](bytes)(CompilationTrace.codec) + + assert(trace.project == "a") + assert(trace.files.exists(_.endsWith("Foo.scala"))) + assert(trace.diagnostics.isEmpty) + assert(!trace.isNoOp) + } + } + + test("compilation trace contains diagnostics") { + TestUtil.withinWorkspace { workspace => + val sources = List( + """/main/scala/Foo.scala + |class Foo { + | def bar: Int = "string" + |} + """.stripMargin + ) + + val logger = new RecordingLogger(ansiCodesSupported = false, debug = false) + val `A` = TestProject(workspace, "a", sources) + val projects = List(`A`) + + val settings = WorkspaceSettings( + None, + None, + None, + None, + Some(TraceSettings(None, None, None, None, None, None, None, Some(true))), + None + ) + val configDir = workspace.resolve(".bloop") + if (!Files.exists(configDir.underlying)) Files.createDirectories(configDir.underlying) + WorkspaceSettings.writeToFile(configDir, settings, logger) + Files.createFile(workspace.resolve("compilation-trace.json").underlying) + + val state = loadState(workspace, projects, logger) + val compiledState = state.compile(`A`) + assertExitStatus(compiledState, ExitStatus.CompilationError) + + val traceFile = workspace.resolve("compilation-trace-a.json") + assert(Files.exists(traceFile.underlying)) + + val bytes = Files.readAllBytes(traceFile.underlying) + val trace = readFromArray[CompilationTrace](bytes)(CompilationTrace.codec) + + assert(trace.diagnostics.nonEmpty) + assert(trace.diagnostics.exists(_.message.contains("type mismatch"))) + } + } + + test("compilation trace records no-op") { + TestUtil.withinWorkspace { workspace => + val sources = List( + """/main/scala/Foo.scala + |class Foo + """.stripMargin + ) + + val logger = new RecordingLogger(ansiCodesSupported = false, debug = false) + val `A` = TestProject(workspace, "a", sources) + val projects = List(`A`) + + val settings = WorkspaceSettings( + None, + None, + None, + None, + Some(TraceSettings(None, None, None, None, None, None, None, Some(true))), + None + ) + val configDir = workspace.resolve(".bloop") + if (!Files.exists(configDir.underlying)) Files.createDirectories(configDir.underlying) + WorkspaceSettings.writeToFile(configDir, settings, logger) + Files.createFile(workspace.resolve("compilation-trace.json").underlying) + + val state = loadState(workspace, projects, logger) + val compiledState = state.compile(`A`) + assertExitStatus(compiledState, ExitStatus.Ok) + + val secondCompiledState = compiledState.compile(`A`) + assertExitStatus(secondCompiledState, ExitStatus.Ok) + + val traceFile = workspace.resolve("compilation-trace-a.json") + val bytes = Files.readAllBytes(traceFile.underlying) + val trace = readFromArray[CompilationTrace](bytes)(CompilationTrace.codec) + + // Since we overwrite, it should be the trace of the last compilation (no-op) + assert(trace.isNoOp) + } + } +} diff --git a/frontend/src/test/scala/bloop/DagSpec.scala b/frontend/src/test/scala/bloop/DagSpec.scala index cb85672860..351113e623 100644 --- a/frontend/src/test/scala/bloop/DagSpec.scala +++ b/frontend/src/test/scala/bloop/DagSpec.scala @@ -27,7 +27,7 @@ class DagSpec { // format: OFF def dummyOrigin: Origin = TestUtil.syntheticOriginFor(dummyPath) def dummyProject(name: String, dependencies: List[String]): Project = - Project(name, dummyPath, None, dependencies, Some(dummyInstance), Nil, Nil, compileOptions, + Project(name, dummyPath, None, dependencies, Some(dummyInstance), Nil, Nil, Nil, compileOptions, dummyPath, isBestEffort = false, Nil, Nil, Nil, Nil, None, Nil, Nil, Config.TestOptions.empty, dummyPath, dummyPath, Project.defaultPlatform(logger, Nil, Nil), None, None, Nil, dummyOrigin) // format: ON diff --git a/frontend/src/test/scala/bloop/ResourceMappingIntegrationSpec.scala b/frontend/src/test/scala/bloop/ResourceMappingIntegrationSpec.scala new file mode 100644 index 0000000000..9a9a69bb41 --- /dev/null +++ b/frontend/src/test/scala/bloop/ResourceMappingIntegrationSpec.scala @@ -0,0 +1,172 @@ +package bloop + +import java.nio.file.Files +import bloop.cli.ExitStatus +import bloop.io.AbsolutePath +import bloop.logging.RecordingLogger +import bloop.util.TestProject +import bloop.util.TestUtil +import bloop.data.LoadedProject + +object ResourceMappingIntegrationSpec extends bloop.testing.BaseSuite { + + test("compile with resource mappings copies files to classes directory") { + TestUtil.withinWorkspace { workspace => + val logger = new RecordingLogger(ansiCodesSupported = false) + + // Create a simple project + val source = + """/main/scala/Foo.scala + |class Foo + """.stripMargin + + val `A` = TestProject(workspace, "a", List(source)) + val projects = List(`A`) + val state = loadState(workspace, projects, logger) + + // Create a resource file to map + val resourceFile = workspace.resolve("custom-resource.txt") + Files.write(resourceFile.underlying, "resource content".getBytes("UTF-8")) + + // Get the project and inject mappings manually + val projectA = state.getProjectFor(`A`) + val mappings = List((resourceFile, "assets/config.txt")) + val projectWithMappings = projectA.copy(resourceMappings = mappings) + + // Update state with modified project + val stateWithMappings = new TestState( + state.state.copy( + build = state.state.build.copy( + loadedProjects = state.state.build.loadedProjects.map { lp => + if (lp.project.name == projectA.name) { + lp match { + case LoadedProject.RawProject(_) => LoadedProject.RawProject(projectWithMappings) + case LoadedProject.ConfiguredProject(_, original, settings) => + LoadedProject.ConfiguredProject(projectWithMappings, original, settings) + } + } else lp + } + ) + ) + ) + + // Compile + val compiledState = stateWithMappings.compile(`A`) + assertExitStatus(compiledState, ExitStatus.Ok) + + // Verify resource was copied to classes directory + val classesDir = compiledState.getClientExternalDir(`A`) + val copiedResource = classesDir.resolve("assets/config.txt") + + try { + assert(copiedResource.exists) + val content = new String(Files.readAllBytes(copiedResource.underlying), "UTF-8") + assertNoDiff(content, "resource content") + } catch { + case t: Throwable => + logger.dump() + throw t + } + } + } + + test("compile with directory mapping copies entire directory") { + TestUtil.withinWorkspace { workspace => + val logger = new RecordingLogger(ansiCodesSupported = false) + + val `A` = TestProject(workspace, "a", List("/main/scala/Foo.scala\nclass Foo")) + val projects = List(`A`) + val state = loadState(workspace, projects, logger) + + // Create a directory structure to map + val resourceDir = workspace.resolve("resources") + Files.createDirectories(resourceDir.underlying) + Files.write(resourceDir.resolve("file1.txt").underlying, "content 1".getBytes("UTF-8")) + val subDir = resourceDir.resolve("sub") + Files.createDirectories(subDir.underlying) + Files.write(subDir.resolve("file2.txt").underlying, "content 2".getBytes("UTF-8")) + + // Inject mappings + val projectA = state.getProjectFor(`A`) + val mappings = List((resourceDir, "data")) + val projectWithMappings = projectA.copy(resourceMappings = mappings) + + val stateWithMappings = new TestState( + state.state.copy( + build = state.state.build.copy( + loadedProjects = state.state.build.loadedProjects.map { lp => + if (lp.project.name == projectA.name) { + lp match { + case LoadedProject.RawProject(_) => LoadedProject.RawProject(projectWithMappings) + case LoadedProject.ConfiguredProject(_, original, settings) => + LoadedProject.ConfiguredProject(projectWithMappings, original, settings) + } + } else lp + } + ) + ) + ) + + // Compile + val compiledState = stateWithMappings.compile(`A`) + assertExitStatus(compiledState, ExitStatus.Ok) + + // Verify directory structure copied + val classesDir = compiledState.getClientExternalDir(`A`) + assert(classesDir.resolve("data/file1.txt").exists) + assert(classesDir.resolve("data/sub/file2.txt").exists) + } + } + + test("resources are copied even if compilation is no-op") { + TestUtil.withinWorkspace { workspace => + val logger = new RecordingLogger(ansiCodesSupported = false) + + val `A` = TestProject(workspace, "a", List("/main/scala/Foo.scala\nclass Foo")) + val projects = List(`A`) + val state = loadState(workspace, projects, logger) + + val resourceFile = workspace.resolve("res.txt") + Files.write(resourceFile.underlying, "v1".getBytes("UTF-8")) + + val projectA = state.getProjectFor(`A`) + val mappings = List((resourceFile, "res.txt")) + val projectWithMappings = projectA.copy(resourceMappings = mappings) + + val stateWithMappings = new TestState( + state.state.copy( + build = state.state.build.copy( + loadedProjects = state.state.build.loadedProjects.map { lp => + if (lp.project.name == projectA.name) { + lp match { + case LoadedProject.RawProject(_) => LoadedProject.RawProject(projectWithMappings) + case LoadedProject.ConfiguredProject(_, original, settings) => + LoadedProject.ConfiguredProject(projectWithMappings, original, settings) + } + } else lp + } + ) + ) + ) + + // First compile + val compiledState = stateWithMappings.compile(`A`) + assertExitStatus(compiledState, ExitStatus.Ok) + + // Verify v1 + val classesDir = compiledState.getClientExternalDir(`A`) + val copiedFile = classesDir.resolve("res.txt") + assertNoDiff(new String(Files.readAllBytes(copiedFile.underlying)), "v1") + + // Update resource + Files.write(resourceFile.underlying, "v2".getBytes("UTF-8")) + + // Compile again (should be no-op for scala, but resources should update) + val compiledState2 = compiledState.compile(`A`) + assertExitStatus(compiledState2, ExitStatus.Ok) + + // Verify v2 + assertNoDiff(new String(Files.readAllBytes(copiedFile.underlying)), "v2") + } + } +} diff --git a/frontend/src/test/scala/bloop/util/TestUtil.scala b/frontend/src/test/scala/bloop/util/TestUtil.scala index 58f8645376..4d07c95fb2 100644 --- a/frontend/src/test/scala/bloop/util/TestUtil.scala +++ b/frontend/src/test/scala/bloop/util/TestUtil.scala @@ -411,6 +411,7 @@ object TestUtil { scalaInstance = scalaInstance, rawClasspath = classpath, resources = Nil, + resourceMappings = Nil, isBestEffort = false, compileSetup = Config.CompileSetup.empty.copy(order = compileOrder), genericClassesDir = classes,