From 9f7899a601198405bf3eb4f8c9171eb979d0bfbe Mon Sep 17 00:00:00 2001 From: kasiaMarek Date: Mon, 9 Dec 2024 17:48:38 +0100 Subject: [PATCH] don't call no op compilations --- .../meta/internal/metals/Compilations.scala | 60 ++++---- .../meta/internal/metals/FileChanges.scala | 131 ++++++++++++++++++ .../internal/metals/MetalsLspService.scala | 56 ++------ .../metals/MutableMd5Fingerprints.scala | 6 +- .../metals/ProjectMetalsLspService.scala | 4 +- .../internal/metals/SavedFileSignatures.scala | 66 --------- .../scala/tests/sbt/CancelCompileSuite.scala | 7 +- .../test/scala/tests/sbt/SbtServerSuite.scala | 3 +- .../src/main/scala/tests/TestingServer.scala | 6 +- .../src/test/scala/tests/BillLspSuite.scala | 9 +- .../scala/tests/CancelCompileLspSuite.scala | 7 +- .../tests/UnsupportedDebuggingLspSuite.scala | 5 +- 12 files changed, 192 insertions(+), 168 deletions(-) create mode 100644 metals/src/main/scala/scala/meta/internal/metals/FileChanges.scala delete mode 100644 metals/src/main/scala/scala/meta/internal/metals/SavedFileSignatures.scala diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilations.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilations.scala index 7d0969a7a30..34dc234a53f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilations.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilations.scala @@ -34,7 +34,7 @@ final class Compilations( downstreamTargets: PreviouslyCompiledDownsteamTargets, bestEffortEnabled: Boolean, )(implicit ec: ExecutionContext) { - private val fileSignatures = new SavedFileSignatures + private val fileChanges = new FileChanges(buildTargets, workspace) private val compileTimeout: Timeout = Timeout("compile", Duration(10, TimeUnit.MINUTES)) private val cascadeTimeout: Timeout = @@ -97,6 +97,7 @@ final class Compilations( def compileTarget( target: b.BuildTargetIdentifier ): Future[b.CompileResult] = { + fileChanges.willCompile(List(target)) compileBatch(target).map { results => results.getOrElse(target, new b.CompileResult(b.StatusCode.CANCELLED)) } @@ -105,48 +106,52 @@ final class Compilations( def compileTargets( targets: Seq[b.BuildTargetIdentifier] ): Future[Unit] = { + fileChanges.willCompile(targets.toList) compileBatch(targets).ignoreValue } - def compileFile(path: PathWithContent): Future[Option[b.CompileResult]] = { - if (fileSignatures.didSavedContentChanged(path)) { - def empty = new b.CompileResult(b.StatusCode.CANCELLED) - for { - targetOpt <- expand(path.path) - result <- targetOpt match { - case None => Future.successful(empty) - case Some(target) => - compileBatch(target) - .map(res => res.getOrElse(target, empty)) - } - _ <- compileWorksheets(Seq(path.path)) - } yield Some(result) - } else Future.successful(None) + def compileFile( + path: AbsolutePath, + fingerprint: Option[Fingerprint] = None, + assumeDidNotChange: Boolean = false, + ): Future[Option[b.CompileResult]] = { + def empty = new b.CompileResult(b.StatusCode.CANCELLED) + for { + targetOpt <- fileChanges.buildTargetToCompile( + path, + fingerprint, + assumeDidNotChange, + ) + result <- targetOpt match { + case None => Future.successful(empty) + case Some(target) => + compileBatch(target) + .map(res => res.getOrElse(target, empty)) + } + _ <- + if (assumeDidNotChange && targetOpt.isEmpty) Future.successful(()) + else compileWorksheets(Seq(path)) + } yield Some(result) } def compileFiles( - pathsWithContent: Seq[PathWithContent], + paths: Seq[(AbsolutePath, Fingerprint)], focusedDocumentBuildTarget: Option[BuildTargetIdentifier], ): Future[Unit] = { - val paths = - pathsWithContent.filter(fileSignatures.didSavedContentChanged).map(_.path) for { - targets <- expand(paths) + targets <- fileChanges.buildTargetsToCompile( + paths, + focusedDocumentBuildTarget, + ) _ <- compileBatch(targets) - _ <- focusedDocumentBuildTarget match { - case Some(bt) - if !targets.contains(bt) && - buildTargets.isInverseDependency(bt, targets.toList) => - compileBatch(bt) - case _ => Future.successful(()) - } - _ <- compileWorksheets(paths) + _ <- compileWorksheets(paths.map(_._1)) } yield () } def cascadeCompile(targets: Seq[BuildTargetIdentifier]): Future[Unit] = { val inverseDependencyLeaves = targets.flatMap(buildTargets.inverseDependencyLeaves).distinct + fileChanges.willCompile(inverseDependencyLeaves.toList) cascadeBatch(inverseDependencyLeaves).map(_ => ()) } @@ -162,7 +167,6 @@ final class Compilations( lastCompile = Set.empty cascadeBatch.cancelAll() compileBatch.cancelAll() - fileSignatures.cancel() } def recompileAll(): Future[Unit] = { diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileChanges.scala b/metals/src/main/scala/scala/meta/internal/metals/FileChanges.scala new file mode 100644 index 00000000000..5c6b17d272f --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/FileChanges.scala @@ -0,0 +1,131 @@ +package scala.meta.internal.metals + +import scala.collection.concurrent.TrieMap +import scala.collection.mutable +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.io.AbsolutePath + +import ch.epfl.scala.bsp4j.BuildTargetIdentifier + +class FileChanges(buildTargets: BuildTargets, workspace: () => AbsolutePath)( + implicit ec: ExecutionContext +) { + private val previousCreateOrModify = TrieMap[AbsolutePath, String]() + private val dirtyBuildTargets = mutable.Set[BuildTargetIdentifier]() + + def buildTargetsToCompile( + paths: Seq[(AbsolutePath, Fingerprint)], + focusedDocumentBuildTarget: Option[BuildTargetIdentifier], + ): Future[Seq[BuildTargetIdentifier]] = + for { + toCompile <- paths.foldLeft( + Future.successful(Seq.empty[BuildTargetIdentifier]) + ) { case (toCompile, (path, fingerprint)) => + toCompile.flatMap { acc => + findBuildTargetIfShouldCompile(path, Some(fingerprint)).map(acc ++ _) + } + } + } yield { + val allToCompile = + if (focusedDocumentBuildTarget.exists(dirtyBuildTargets(_))) + toCompile ++ focusedDocumentBuildTarget + else toCompile + willCompile(allToCompile) + allToCompile + } + + def buildTargetToCompile( + path: AbsolutePath, + fingerprint: Option[Fingerprint], + assumeDidNotChange: Boolean, + ): Future[Option[BuildTargetIdentifier]] = { + for { + toCompile <- findBuildTargetIfShouldCompile( + path, + fingerprint, + assumeDidNotChange, + ) + } yield { + willCompile(toCompile.toSeq) + toCompile + } + } + + def willCompile(ids: Seq[BuildTargetIdentifier]): Unit = + buildTargets + .buildTargetTransitiveDependencies(ids.toList) + .foreach(dirtyBuildTargets.remove) + + private def findBuildTargetIfShouldCompile( + path: AbsolutePath, + fingerprint: Option[Fingerprint], + assumeDidNotChange: Boolean = false, + ): Future[Option[BuildTargetIdentifier]] = { + expand(path).map( + _.filter(bt => + dirtyBuildTargets.contains( + bt + ) || (!assumeDidNotChange && didContentChange(path, fingerprint, bt)) + ) + ) + } + + private def expand( + path: AbsolutePath + ): Future[Option[BuildTargetIdentifier]] = { + val isCompilable = + (path.isScalaOrJava || path.isSbt) && + !path.isDependencySource(workspace()) && + !path.isInTmpDirectory(workspace()) + + if (isCompilable) { + val targetOpt = buildTargets.inverseSourcesBsp(path) + targetOpt.foreach { + case tgts if tgts.isEmpty => scribe.warn(s"no build target for: $path") + case _ => + } + + targetOpt + } else + Future.successful(None) + } + + private def didContentChange( + path: AbsolutePath, + fingerprint: Option[Fingerprint], + buildTarget: BuildTargetIdentifier, + ): Boolean = { + val didChange = didContentChange(path, fingerprint) + if (didChange) { + buildTargets + .allInverseDependencies(buildTarget) + .foreach { bt => + if (bt != buildTarget) dirtyBuildTargets.add(bt) + } + } + didChange + } + + private def didContentChange( + path: AbsolutePath, + fingerprint: Option[Fingerprint], + ): Boolean = { + fingerprint + .map { fingerprint => + synchronized { + if (previousCreateOrModify.getOrElse(path, null) == fingerprint.md5) + false + else { + previousCreateOrModify.put(path, fingerprint.md5) + true + } + } + } + .getOrElse(true) + } + + def cancel(): Unit = previousCreateOrModify.clear() +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 5545c3aaeb7..9538128f2d3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -744,15 +744,9 @@ abstract class MetalsLspService( // In some cases like peeking definition didOpen might be followed up by close // and we would lose the notion of the focused document recentlyOpenedFiles.add(path) - val prevBuildTarget = focusedDocumentBuildTarget.getAndUpdate { current => - buildTargets - .inverseSources(path) - .getOrElse(current) - } - val content = FileIO.readAllBytes(path) // Update md5 fingerprint from file contents on disk - fingerprints.add(path, new String(content, charset)) + val fingerprint = fingerprints.add(path, FileIO.slurp(path, charset)) // Update in-memory buffer contents from LSP client buffers.put(path, params.getTextDocument.getText) @@ -785,10 +779,7 @@ abstract class MetalsLspService( Future .sequence( List( - maybeCompileOnDidFocus( - PathWithContent(path, content), - prevBuildTarget, - ), + compilations.compileFile(path, Some(fingerprint)), compilers.load(List(path)), parser, interactive, @@ -811,11 +802,6 @@ abstract class MetalsLspService( uri: String ): CompletableFuture[DidFocusResult.Value] = { val path = uri.toAbsolutePath - val prevBuildTarget = focusedDocumentBuildTarget.getAndUpdate { current => - buildTargets - .inverseSources(path) - .getOrElse(current) - } scalaCli.didFocus(path) // Don't trigger compilation on didFocus events under cascade compilation // because save events already trigger compile in inverse dependencies. @@ -825,29 +811,16 @@ abstract class MetalsLspService( CompletableFuture.completedFuture(DidFocusResult.RecentlyActive) } else { worksheetProvider.onDidFocus(path) - maybeCompileOnDidFocus(PathWithContent(path), prevBuildTarget).asJava + compilations + .compileFile(path, assumeDidNotChange = true) + .map( + _.map(_ => DidFocusResult.Compiled) + .getOrElse(DidFocusResult.AlreadyCompiled) + ) + .asJava } } - protected def maybeCompileOnDidFocus( - path: PathWithContent, - prevBuildTarget: b.BuildTargetIdentifier, - ): Future[DidFocusResult.Value] = - buildTargets.inverseSources(path.path) match { - case Some(target) if prevBuildTarget != target => - compilations - .compileFile(path) - .map(_ => DidFocusResult.Compiled) - case _ if path.path.isWorksheet => - compilations - .compileFile(path) - .map(_ => DidFocusResult.Compiled) - case Some(_) => - Future.successful(DidFocusResult.AlreadyCompiled) - case None => - Future.successful(DidFocusResult.NoBuildTarget) - } - def pause(): Unit = pauseables.pause() def unpause(): Unit = pauseables.unpause() @@ -942,11 +915,10 @@ abstract class MetalsLspService( } protected def onChange(paths: Seq[AbsolutePath]): Future[Unit] = { - val pathsWithContent = + val pathsWithFingerPrints = paths.map { path => - val content = FileIO.readAllBytes(path) - fingerprints.add(path, new String(content, charset)) - PathWithContent(path, content) + val fingerprint = fingerprints.add(path, FileIO.slurp(path, charset)) + (path, fingerprint) } Future @@ -955,7 +927,7 @@ abstract class MetalsLspService( Future(indexer.reindexWorkspaceSources(paths)), compilations .compileFiles( - pathsWithContent, + pathsWithFingerPrints, Option(focusedDocumentBuildTarget.get()), ), ) ++ paths.map(f => Future(interactiveSemanticdbs.textDocument(f))) @@ -969,7 +941,7 @@ abstract class MetalsLspService( List( compilations .compileFiles( - List(PathWithContent.deleted(path)), + List((path, null)), Option(focusedDocumentBuildTarget.get()), ), Future { diff --git a/metals/src/main/scala/scala/meta/internal/metals/MutableMd5Fingerprints.scala b/metals/src/main/scala/scala/meta/internal/metals/MutableMd5Fingerprints.scala index 105687cb8f3..3bc468bcec5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MutableMd5Fingerprints.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MutableMd5Fingerprints.scala @@ -33,8 +33,10 @@ final class MutableMd5Fingerprints extends Md5Fingerprints { path: AbsolutePath, text: String, md5: Option[String] = None, - ): Unit = { - add(path, Fingerprint(text, md5.getOrElse(MD5.compute(text)))) + ): Fingerprint = { + val fingerprint = Fingerprint(text, md5.getOrElse(MD5.compute(text))) + add(path, fingerprint) + fingerprint } private def add( diff --git a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala index 55e8145c39f..79c005ce21f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala @@ -635,9 +635,7 @@ class ProjectMetalsLspService( for { _ <- connect(ImportBuildAndIndex(session)) } { - focusedDocument().foreach(path => - compilations.compileFile(PathWithContent(path)) - ) + focusedDocument().foreach(path => compilations.compileFile(path)) } } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/SavedFileSignatures.scala b/metals/src/main/scala/scala/meta/internal/metals/SavedFileSignatures.scala deleted file mode 100644 index 453915362bc..00000000000 --- a/metals/src/main/scala/scala/meta/internal/metals/SavedFileSignatures.scala +++ /dev/null @@ -1,66 +0,0 @@ -package scala.meta.internal.metals - -import java.io.IOException - -import scala.collection.concurrent.TrieMap - -import scala.meta.internal.io.FileIO -import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.mtags.MD5 -import scala.meta.io.AbsolutePath - -class SavedFileSignatures { - private val previousCreateOrModify = TrieMap[AbsolutePath, String]() - - def didSavedContentChanged(pathWithContent: PathWithContent): Boolean = { - val path = pathWithContent.path - pathWithContent - .getSignature() - .map { md5 => - synchronized { - if (previousCreateOrModify.get(path) == md5) false - else { - md5 match { - case None => previousCreateOrModify.remove(path) - case Some(md5) => previousCreateOrModify.put(path, md5) - } - true - } - } - } - .getOrElse(true) - } - - def cancel(): Unit = previousCreateOrModify.clear() -} - -class PathWithContent( - val path: AbsolutePath, - optContent: Option[PathWithContent.Content], -) { - def getSignature(): Either[IOException, Option[String]] = { - optContent - .map(_.map(content => MD5.bytesToHex(content))) - .map(Right[IOException, Option[String]](_)) - .getOrElse { - try { - if (path.exists) - Right(Some(MD5.bytesToHex(FileIO.readAllBytes(path)))) - else Right(None) - } catch { - case e: IOException => - scribe.warn(s"Failed to read contents of $path", e) - Left(e) - } - } - } -} - -object PathWithContent { - // None if the file doesn't exist - type Content = Option[Array[Byte]] - def apply(path: AbsolutePath) = new PathWithContent(path, None) - def apply(path: AbsolutePath, content: Array[Byte]) = - new PathWithContent(path, Some(Some(content))) - def deleted(path: AbsolutePath) = new PathWithContent(path, Some(None)) -} diff --git a/tests/slow/src/test/scala/tests/sbt/CancelCompileSuite.scala b/tests/slow/src/test/scala/tests/sbt/CancelCompileSuite.scala index cbd08ea62b5..c5722c80e61 100644 --- a/tests/slow/src/test/scala/tests/sbt/CancelCompileSuite.scala +++ b/tests/slow/src/test/scala/tests/sbt/CancelCompileSuite.scala @@ -1,7 +1,6 @@ package tests import scala.meta.internal.metals.MetalsServerConfig -import scala.meta.internal.metals.PathWithContent import scala.meta.internal.metals.ServerCommands import scala.meta.internal.metals.{BuildInfo => V} @@ -58,9 +57,7 @@ class CancelCompileSuite ) _ <- server.server.buildServerPromise.future (compileReport, _) <- server.server.compilations - .compileFile( - PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala")) - ) + .compileFile(workspace.resolve("c/src/main/scala/c/C.scala")) .zip { // wait until the compilation start Thread.sleep(1000) @@ -69,7 +66,7 @@ class CancelCompileSuite _ = assertNoDiff(client.workspaceDiagnostics, "") _ = assertEquals(compileReport.get.getStatusCode(), StatusCode.CANCELLED) _ <- server.server.compilations.compileFile( - PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala")) + workspace.resolve("c/src/main/scala/c/C.scala") ) _ = assertNoDiff( client.workspaceDiagnostics, diff --git a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala index 508670c6ff3..3cb60b7492c 100644 --- a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala +++ b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala @@ -9,7 +9,6 @@ import scala.meta.internal.metals.CreateSession import scala.meta.internal.metals.Messages import scala.meta.internal.metals.Messages.ImportBuildChanges import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.PathWithContent import scala.meta.internal.metals.ServerCommands import scala.meta.internal.metals.{BuildInfo => V} import scala.meta.internal.process.SystemProcess @@ -410,7 +409,7 @@ class SbtServerSuite _ <- initialize(layout) // make sure to compile once _ <- server.server.compilations.compileFile( - PathWithContent(workspace.resolve("target/Foo.scala")) + workspace.resolve("target/Foo.scala") ) } yield { // Sleep 100 ms: that should be enough to see the compilation looping diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index db10b784a24..dc55fd10bad 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -1060,7 +1060,7 @@ final case class TestingServer( fullServer .getServiceFor(path) .compilations - .compileFile(m.internal.metals.PathWithContent(path)) + .compileFile(path) ) for { @@ -1122,9 +1122,7 @@ final case class TestingServer( codeLenses.trySuccess(lenses.toList) else if (retries > 0) { retries -= 1 - server.compilations.compileFile( - m.internal.metals.PathWithContent(path) - ) + server.compilations.compileFile(path) } else { val error = s"Could not fetch any code lenses in $maxRetries tries" codeLenses.tryFailure(new NoSuchElementException(error)) diff --git a/tests/unit/src/test/scala/tests/BillLspSuite.scala b/tests/unit/src/test/scala/tests/BillLspSuite.scala index 45a5ec7fd68..ffdd7dd17e3 100644 --- a/tests/unit/src/test/scala/tests/BillLspSuite.scala +++ b/tests/unit/src/test/scala/tests/BillLspSuite.scala @@ -5,7 +5,6 @@ import scala.concurrent.Future import scala.meta.internal.metals.Directories import scala.meta.internal.metals.Messages import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.PathWithContent import scala.meta.internal.metals.RecursivelyDelete import scala.meta.internal.metals.ServerCommands import scala.meta.io.AbsolutePath @@ -241,9 +240,7 @@ class BillLspSuite extends BaseLspSuite("bill") { |""".stripMargin ) (compileReport, _) <- server.server.compilations - .compileFile( - PathWithContent(workspace.resolve("src/com/App.scala")) - ) + .compileFile(workspace.resolve("src/com/App.scala")) .zip { // wait until the compilation start while (!trace.contains(s"buildTarget/compile")) { @@ -260,9 +257,7 @@ class BillLspSuite extends BaseLspSuite("bill") { cancelId = cancelMatch.get.group(1) _ = assert(currentTrace.contains(s"buildTarget/compile - ($cancelId)")) compileReport <- server.server.compilations - .compileFile( - PathWithContent(workspace.resolve("src/com/App.scala")) - ) + .compileFile(workspace.resolve("src/com/App.scala")) _ = assertEquals(compileReport.get.getStatusCode(), StatusCode.OK) } yield () } diff --git a/tests/unit/src/test/scala/tests/CancelCompileLspSuite.scala b/tests/unit/src/test/scala/tests/CancelCompileLspSuite.scala index 4f9e0e54d3f..42fe4cac273 100644 --- a/tests/unit/src/test/scala/tests/CancelCompileLspSuite.scala +++ b/tests/unit/src/test/scala/tests/CancelCompileLspSuite.scala @@ -1,6 +1,5 @@ package tests -import scala.meta.internal.metals.PathWithContent import scala.meta.internal.metals.ServerCommands import ch.epfl.scala.bsp4j.StatusCode @@ -48,9 +47,7 @@ class CancelCompileLspSuite extends BaseLspSuite("compile-cancel") { ) _ <- server.server.buildServerPromise.future (compileReport, _) <- server.server.compilations - .compileFile( - PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala")) - ) + .compileFile(workspace.resolve("c/src/main/scala/c/C.scala")) .zip { // wait until the compilation start Thread.sleep(1000) @@ -59,7 +56,7 @@ class CancelCompileLspSuite extends BaseLspSuite("compile-cancel") { _ = assertNoDiff(client.workspaceDiagnostics, "") _ = assertEquals(compileReport.get.getStatusCode(), StatusCode.CANCELLED) _ <- server.server.compilations.compileFile( - PathWithContent(workspace.resolve("c/src/main/scala/c/C.scala")) + workspace.resolve("c/src/main/scala/c/C.scala") ) _ = assertNoDiff( client.workspaceDiagnostics, diff --git a/tests/unit/src/test/scala/tests/UnsupportedDebuggingLspSuite.scala b/tests/unit/src/test/scala/tests/UnsupportedDebuggingLspSuite.scala index c58e7a65a1c..144255114fe 100644 --- a/tests/unit/src/test/scala/tests/UnsupportedDebuggingLspSuite.scala +++ b/tests/unit/src/test/scala/tests/UnsupportedDebuggingLspSuite.scala @@ -9,7 +9,6 @@ import scala.util.Success import scala.meta.internal.metals.ClientCommands import scala.meta.internal.metals.InitializationOptions import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.PathWithContent class UnsupportedDebuggingLspSuite extends BaseLspSuite("unsupported-debugging") { @@ -63,9 +62,7 @@ class UnsupportedDebuggingLspSuite ) _ <- server.server.compilations - .compileFile( - PathWithContent(server.toPath("a/src/main/scala/Main.scala")) - ) + .compileFile(server.toPath("a/src/main/scala/Main.scala")) } yield { val clientCommands = client.clientCommands.asScala.map(_.getCommand).toSet assert(!clientCommands.contains(ClientCommands.RefreshModel.id))