Skip to content

Commit 4216a39

Browse files
Fix: tests loading resources don't work in Intellij triggered tests with BSP import (#5202)
Tests that load resources do not work in some BSP clients like Intellij IDEA (#4427) because these BSP clients do not include resources classpath when running the tests from the IDE. Both sbt and maven (and presumably gradle) copy the resources into the compile destination directory, so while it seems like a hack (as the resources can in theory clash with the compiled classes), this seems to be a working solution. We work around this by detecting if the BSP client is Intellij and performing the merging of the `compile` and `resources` outputs into a separate folder (`bspBuildTargetCompileMerged`) for it. Additionally, if `bspBuildTargetCompileMerged` is invoked, it invokes the `bspBuildTargetCompileMerged` of all the transitive dependencies and uses the `dest` dir of `bspBuildTargetCompileMerged` as a part of test runner classpath. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 734e094 commit 4216a39

File tree

11 files changed

+167
-123
lines changed

11 files changed

+167
-123
lines changed

core/api/src/mill/api/internal/api.scala

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,21 @@ trait JavaModuleApi extends ModuleApi {
3030

3131
private[mill] def bspBuildTargetResources: TaskApi[Seq[java.nio.file.Path]]
3232

33-
private[mill] def bspBuildTargetCompile: TaskApi[java.nio.file.Path]
33+
private[mill] def bspBuildTargetCompile(clientType: BspClientType): TaskApi[java.nio.file.Path]
3434

3535
private[mill] def bspLoggingTest: TaskApi[Unit]
3636

37-
private[mill] def bspBuildTargetJavacOptions(clientWantsSemanticDb: Boolean)
37+
private[mill] def bspBuildTargetJavacOptions(
38+
clientType: BspClientType,
39+
clientWantsSemanticDb: Boolean
40+
)
3841
: TaskApi[EvaluatorApi => (java.nio.file.Path, Seq[String], Seq[String])]
3942

40-
private[mill] def bspCompileClasspath: TaskApi[EvaluatorApi => Seq[String]]
43+
private[mill] def bspCompileClasspath(clientType: BspClientType)
44+
: TaskApi[EvaluatorApi => Seq[String]]
4145

4246
private[mill] def bspBuildTargetScalacOptions(
47+
clientType: BspClientType,
4348
enableJvmCompileClasspathProvider: Boolean,
4449
clientWantsSemanticDb: Boolean
4550
): TaskApi[(Seq[String], EvaluatorApi => Seq[String], EvaluatorApi => java.nio.file.Path)]
@@ -183,3 +188,29 @@ trait PathRefApi {
183188
def quick: Boolean
184189
def sig: Int
185190
}
191+
192+
/** Used to handle edge cases for specific BSP clients. */
193+
private[mill] enum BspClientType {
194+
195+
/** Intellij IDEA */
196+
case IntellijBSP
197+
198+
/** Any other BSP client */
199+
case Other(displayName: String)
200+
201+
/**
202+
* Whether we should copy resources into the compile destination directory.
203+
*
204+
* This is needed because some BSP clients (e.g. Intellij) ignore the resources classpath that we supply for it
205+
* when running tests.
206+
*
207+
* Both sbt and maven (and presumably gradle) copy the resources into the compile destination directory, so while it
208+
* seems like a hack, this seems to be a working solution.
209+
*
210+
* @see https://github.com/com-lihaoyi/mill/issues/4427#issuecomment-2908889481
211+
*/
212+
def needsToMergeResourcesIntoCompileDest: Boolean = this match {
213+
case IntellijBSP => true
214+
case Other(_) => false
215+
}
216+
}

core/define/src/mill/define/ExecutionPaths.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ object ExecutionPaths {
2121
targetPath / os.up / s"${targetPath.last}.log"
2222
)
2323
}
24+
2425
def resolve(
2526
outPath: os.Path,
2627
task: Task.Named[?]

libs/androidlib/src/mill/androidlib/AndroidAppModule.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package mill.androidlib
33
import coursier.params.ResolutionParams
44
import mill.*
55
import mill.api.Logger
6-
import mill.api.internal.{BspBuildTarget, EvaluatorApi, internal}
6+
import mill.api.internal.{BspBuildTarget, BspClientType, EvaluatorApi, internal}
77
import mill.define.{ModuleRef, PathRef, Task}
88
import mill.scalalib.*
99
import mill.testrunner.TestResult
@@ -139,7 +139,7 @@ trait AndroidAppModule extends AndroidModule { outer =>
139139
def androidLintArgs: T[Seq[String]] = Task { Seq.empty[String] }
140140

141141
@internal
142-
override def bspCompileClasspath = Task.Anon { (ev: EvaluatorApi) =>
142+
override def bspCompileClasspath(clientType: BspClientType) = Task.Anon { (ev: EvaluatorApi) =>
143143
compileClasspath().map(
144144
_.path
145145
).map(UnresolvedPath.ResolvedPath(_)).map(_.resolve(os.Path(ev.outPathJava))).map(sanitizeUri)

libs/scalalib/src/mill/scalalib/JavaModule.scala

Lines changed: 102 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,38 @@
11
package mill
22
package scalalib
33

4-
import scala.util.chaining.scalaUtilChainingOps
5-
import scala.util.matching.Regex
6-
7-
import coursier.Repository
8-
import coursier.Type
9-
import coursier.core as cs
10-
import coursier.core.BomDependency
11-
import coursier.core.Configuration
12-
import coursier.core.DependencyManagement
13-
import coursier.core.Resolution
4+
import coursier.{Repository, Type, core as cs}
5+
import coursier.core.{BomDependency, Configuration, DependencyManagement, Resolution}
146
import coursier.params.ResolutionParams
15-
import coursier.parse.JavaOrScalaModule
16-
import coursier.parse.ModuleParser
17-
import coursier.util.EitherT
18-
import coursier.util.ModuleMatcher
19-
import coursier.util.Monad
7+
import coursier.parse.{JavaOrScalaModule, ModuleParser}
8+
import coursier.util.{EitherT, ModuleMatcher, Monad}
209
import mainargs.Flag
21-
import mill.api.MillException
22-
import mill.api.Result
23-
import mill.api.Segments
24-
import mill.api.internal.BspBuildTarget
25-
import mill.api.internal.BspModuleApi
26-
import mill.api.internal.BspUri
27-
import mill.api.internal.EvaluatorApi
28-
import mill.api.internal.IdeaConfigFile
29-
import mill.api.internal.JavaFacet
30-
import mill.api.internal.JavaModuleApi
31-
import mill.api.internal.JvmBuildTarget
32-
import mill.api.internal.ResolvedModule
33-
import mill.api.internal.Scoped
34-
import mill.api.internal.internal
35-
import mill.define.ModuleRef
36-
import mill.define.PathRef
37-
import mill.define.Segment
38-
import mill.define.Task
39-
import mill.define.TaskCtx
40-
import mill.define.TaskModule
10+
import mill.api.{MillException, Result, Segments}
11+
import mill.api.internal.{
12+
BspBuildTarget,
13+
BspClientType,
14+
BspModuleApi,
15+
BspUri,
16+
EvaluatorApi,
17+
IdeaConfigFile,
18+
JavaFacet,
19+
JavaModuleApi,
20+
JvmBuildTarget,
21+
ResolvedModule,
22+
Scoped,
23+
internal
24+
}
25+
import mill.define.{ModuleRef, PathRef, Segment, Task, TaskCtx, TaskModule}
4126
import mill.scalalib.api.CompilationResult
4227
import mill.scalalib.bsp.BspModule
4328
import mill.scalalib.internal.ModuleUtils
4429
import mill.scalalib.publish.Artifact
45-
import mill.util.JarManifest
46-
import mill.util.Jvm
30+
import mill.util.{JarManifest, Jvm}
4731
import os.Path
4832

33+
import scala.util.chaining.scalaUtilChainingOps
34+
import scala.util.matching.Regex
35+
4936
/**
5037
* Core configuration required to compile a single Java compilation target
5138
*/
@@ -695,17 +682,6 @@ trait JavaModule
695682
Task.traverse(transitiveModuleCompileModuleDeps)(_.jar)()
696683
}
697684

698-
/**
699-
* Same as [[transitiveLocalClasspath]], but with all dependencies on [[compile]]
700-
* replaced by their non-compiling [[bspCompileClassesPath]] variants.
701-
*
702-
* Keep in sync with [[transitiveLocalClasspath]]
703-
*/
704-
@internal
705-
private[mill] def bspTransitiveLocalClasspath: T[Seq[UnresolvedPath]] = Task {
706-
Task.traverse(transitiveModuleCompileModuleDeps)(_.bspLocalClasspath)().flatten
707-
}
708-
709685
/**
710686
* The transitive version of `compileClasspath`
711687
*/
@@ -722,11 +698,14 @@ trait JavaModule
722698
* Keep in sync with [[transitiveCompileClasspath]]
723699
*/
724700
@internal
725-
private[mill] def bspTransitiveCompileClasspath: T[Seq[UnresolvedPath]] = Task {
701+
private[mill] def bspTransitiveCompileClasspath(clientType: BspClientType)
702+
: Task[Seq[UnresolvedPath]] = Task.Anon {
726703
Task.traverse(transitiveModuleCompileModuleDeps)(m =>
727704
Task.Anon {
728-
m.localCompileClasspath().map(p => UnresolvedPath.ResolvedPath(p.path)) ++
729-
Seq(m.bspCompileClassesPath())
705+
val localCompileClasspath =
706+
m.localCompileClasspath().map(p => UnresolvedPath.ResolvedPath(p.path))
707+
val compileClassesPath = m.bspCompileClassesPath(clientType)()
708+
localCompileClasspath :+ compileClassesPath
730709
}
731710
)()
732711
.flatten
@@ -820,29 +799,32 @@ trait JavaModule
820799
)
821800
}
822801

802+
/** Resolves paths relative to the `out` folder. */
803+
@internal
804+
private[mill] def resolveRelativeToOut(
805+
task: Task.Named[?],
806+
mkPath: os.SubPath => os.SubPath = identity
807+
): UnresolvedPath.DestPath =
808+
UnresolvedPath.DestPath(mkPath(os.sub), task.ctx.segments)
809+
810+
/** The path where the compiled classes produced by [[compile]] are stored. */
811+
@internal
812+
private[mill] def compileClassesPath: UnresolvedPath.DestPath =
813+
resolveRelativeToOut(compile, _ / "classes")
814+
823815
/**
824816
* The path to the compiled classes by [[compile]] without forcing to actually run the compilation.
825817
* This is safe in an BSP context, as the compilation done later will use the
826818
* exact same compilation settings, so we can safely use the same path.
827819
*
828-
* Keep in sync with [[compile]]
820+
* Keep in sync with [[compile]] and [[bspBuildTargetCompile]].
829821
*/
830822
@internal
831-
private[mill] def bspCompileClassesPath: T[UnresolvedPath] =
832-
if (compile.ctx.enclosing == s"${classOf[JavaModule].getName}#compile") {
833-
Task {
834-
Task.log.debug(
835-
s"compile target was not overridden, assuming hard-coded classes directory for target ${compile}"
836-
)
837-
UnresolvedPath.DestPath(os.sub / "classes", compile.ctx.segments)
838-
}
839-
} else {
840-
Task {
841-
Task.log.debug(
842-
s"compile target was overridden, need to actually execute compilation to get the compiled classes directory for target ${compile}"
843-
)
844-
UnresolvedPath.ResolvedPath(compile().classes.path)
845-
}
823+
private[mill] def bspCompileClassesPath(clientType: BspClientType): Task[UnresolvedPath] =
824+
Task.Anon {
825+
if (clientType.needsToMergeResourcesIntoCompileDest)
826+
resolveRelativeToOut(bspBuildTargetCompileMerged)
827+
else compileClassesPath
846828
}
847829

848830
/**
@@ -851,20 +833,20 @@ trait JavaModule
851833
* Keep in sync with [[bspLocalRunClasspath]]
852834
*/
853835
override def localRunClasspath: T[Seq[PathRef]] = Task {
854-
super.localRunClasspath() ++ resources() ++
855-
Seq(compile().classes)
836+
super.localRunClasspath() ++ resources() ++ Seq(compile().classes)
856837
}
857838

858839
/**
859840
* Same as [[localRunClasspath]] but for use in BSP server.
860841
*
861842
* Keep in sync with [[localRunClasspath]]
862843
*/
863-
private[mill] def bspLocalRunClasspath: T[Seq[UnresolvedPath]] = Task {
864-
Seq.from(super.localRunClasspath() ++ resources())
865-
.map(p => UnresolvedPath.ResolvedPath(p.path)) ++
866-
Seq(bspCompileClassesPath())
867-
}
844+
private[mill] def bspLocalRunClasspath(clientType: BspClientType): Task[Seq[UnresolvedPath]] =
845+
Task.Anon {
846+
Seq.from(super.localRunClasspath() ++ resources())
847+
.map(p => UnresolvedPath.ResolvedPath(p.path)) ++
848+
Seq(bspCompileClassesPath(clientType)())
849+
}
868850

869851
/**
870852
* The *output* classfiles/resources from this module, used for execution,
@@ -886,10 +868,11 @@ trait JavaModule
886868
* Keep in sync with [[localClasspath]]
887869
*/
888870
@internal
889-
private[mill] def bspLocalClasspath: T[Seq[UnresolvedPath]] = Task {
890-
(localCompileClasspath()).map(p => UnresolvedPath.ResolvedPath(p.path)) ++
891-
bspLocalRunClasspath()
892-
}
871+
private[mill] def bspLocalClasspath(clientType: BspClientType): Task[Seq[UnresolvedPath]] =
872+
Task.Anon {
873+
localCompileClasspath().map(p => UnresolvedPath.ResolvedPath(p.path)) ++
874+
bspLocalRunClasspath(clientType)()
875+
}
893876

894877
/**
895878
* All classfiles and resources from upstream modules and dependencies
@@ -907,10 +890,11 @@ trait JavaModule
907890
* Keep in sync with [[compileClasspath]]
908891
*/
909892
@internal
910-
private[mill] def bspCompileClasspath: Task[EvaluatorApi => Seq[String]] = Task.Anon {
893+
private[mill] def bspCompileClasspath(clientType: BspClientType)
894+
: Task[EvaluatorApi => Seq[String]] = Task.Anon {
911895
(ev: EvaluatorApi) =>
912896
(resolvedMvnDeps().map(p => UnresolvedPath.ResolvedPath(p.path)) ++
913-
bspTransitiveCompileClasspath() ++
897+
bspTransitiveCompileClasspath(clientType)() ++
914898
localCompileClasspath().map(p => UnresolvedPath.ResolvedPath(p.path))).map(_.resolve(
915899
os.Path(ev.outPathJava)
916900
)).map(sanitizeUri)
@@ -1345,6 +1329,7 @@ trait JavaModule
13451329
}
13461330

13471331
private[mill] def bspBuildTargetScalacOptions(
1332+
clientType: BspClientType,
13481333
enableJvmCompileClasspathProvider: Boolean,
13491334
clientWantsSemanticDb: Boolean
13501335
) = {
@@ -1359,10 +1344,10 @@ trait JavaModule
13591344
if (enableJvmCompileClasspathProvider) {
13601345
// We have a dedicated request for it
13611346
Task.Anon {
1362-
(e: EvaluatorApi) => Seq.empty[String]
1347+
(_: EvaluatorApi) => Seq.empty[String]
13631348
}
13641349
} else {
1365-
bspCompileClasspath
1350+
bspCompileClasspath(clientType)
13661351
}
13671352

13681353
val classesPathTask =
@@ -1372,7 +1357,7 @@ trait JavaModule
13721357
)
13731358
} else {
13741359
Task.Anon((e: EvaluatorApi) =>
1375-
bspCompileClassesPath().resolve(os.Path(e.outPathJava)).toNIO
1360+
bspCompileClassesPath(clientType)().resolve(os.Path(e.outPathJava)).toNIO
13761361
)
13771362
}
13781363

@@ -1381,17 +1366,20 @@ trait JavaModule
13811366
}
13821367
}
13831368

1384-
private[mill] def bspBuildTargetJavacOptions(clientWantsSemanticDb: Boolean) = {
1369+
private[mill] def bspBuildTargetJavacOptions(
1370+
clientType: BspClientType,
1371+
clientWantsSemanticDb: Boolean
1372+
) = {
13851373
val classesPathTask = this match {
13861374
case sem: SemanticDbJavaModule if clientWantsSemanticDb =>
13871375
sem.bspCompiledClassesAndSemanticDbFiles
1388-
case _ => bspCompileClassesPath
1376+
case _ => bspCompileClassesPath(clientType)
13891377
}
13901378
Task.Anon { (ev: EvaluatorApi) =>
13911379
(
13921380
classesPathTask().resolve(os.Path(ev.outPathJava)).toNIO,
13931381
javacOptions() ++ mandatoryJavacOptions(),
1394-
bspCompileClasspath.apply().apply(ev)
1382+
bspCompileClasspath(clientType).apply().apply(ev)
13951383
)
13961384
}
13971385
}
@@ -1452,7 +1440,34 @@ trait JavaModule
14521440

14531441
private[mill] def bspBuildTargetResources = Task.Anon { resources().map(_.path.toNIO) }
14541442

1455-
private[mill] def bspBuildTargetCompile = Task.Anon { compile().classes.path.toNIO }
1443+
/**
1444+
* Performs the compilation (via [[compile]]) and merging of [[resources]] needed by
1445+
* [[BspClientType.needsToMergeResourcesIntoCompileDest]].
1446+
*/
1447+
private[mill] def bspBuildTargetCompileMerged: T[PathRef] = Task {
1448+
1449+
/**
1450+
* Make sure to invoke the [[bspBuildTargetCompileMerged]] of the transitive dependencies. For example, tests
1451+
* should be able to read resources of the module that they are testing.
1452+
*/
1453+
val _ = Task.traverse(transitiveModuleCompileModuleDeps)(m =>
1454+
Task.Anon(m.bspBuildTargetCompileMerged())
1455+
)()
1456+
1457+
// Merge the compile and resources classpaths.
1458+
os.copy(compile().classes.path, Task.dest, mergeFolders = true)
1459+
resources().foreach { resource =>
1460+
os.copy(resource.path, Task.dest, mergeFolders = true)
1461+
}
1462+
1463+
PathRef(Task.dest)
1464+
}
1465+
1466+
private[mill] def bspBuildTargetCompile(clientType: BspClientType): Task[java.nio.file.Path] = {
1467+
if (clientType.needsToMergeResourcesIntoCompileDest)
1468+
Task.Anon { bspBuildTargetCompileMerged().path.toNIO }
1469+
else Task.Anon { compile().classes.path.toNIO }
1470+
}
14561471

14571472
private[mill] def bspLoggingTest = Task.Anon {
14581473
System.out.println("bspLoggingTest from System.out")

0 commit comments

Comments
 (0)