Skip to content

Commit 6e0de1c

Browse files
authored
Restructure TestModule, add RunModule (#3064)
This idea of this change is to restructure the `TestModule` to decouple it from the concept of compilation. After this change, only a `runClasspath` and a `testClasspath` is needed to run tests. This makes adding test modules for the sake of using a different test framework a breeze. See the following example: the `test2` module is an additional test framework on the same classes of `test`. ```scala import mill._ import mill.scalalib._ import mill.scalalib.api.CompilationResult object foo extends RootModule with ScalaModule { def scalaVersion = "2.13.11" def ivyDeps = Agg( ivy"com.lihaoyi::scalatags:0.12.0", ivy"com.lihaoyi::mainargs:0.6.2" ) object test extends ScalaTests { def ivyDeps = Agg( ivy"com.lihaoyi::utest:0.7.11", ivy"org.scalatest::scalatest-freespec:3.2.18" ) def testFramework = "utest.runner.Framework" } object test2 extends TestModule with TestModule.ScalaTest { override def compile: T[CompilationResult] = ??? override def runClasspath: T[Seq[PathRef]] = foo.test.runClasspath() override def testClasspath = foo.test.testClasspath() } } ``` Please note the `compile` target is a legacy to our binary-compatibility promise. The target is not used directly in `TestModule`. This pull request additionally contains the following changes: * Introduces a new `RunModule` and moved some `run`-related task previously in `TestModule` up. * Extend `RunModule` in `JavaModule` to share run-releated targets and resolve super-hierarchy * Introduces a `WithZincWorker` as a shared base trait to resolve super-hierarchies for using and overriding a common worker. I plan to move more run-releated target from `JavaModule` to `RunModule` in a subsequent PR. (See #3090) See also the following discussion: * #3076 Pull request: #3064
1 parent a5afa5f commit 6e0de1c

File tree

13 files changed

+264
-34
lines changed

13 files changed

+264
-34
lines changed

bsp/worker/src/mill/bsp/worker/MillScalaBuildServer.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ private trait MillScalaBuildServer extends ScalaBuildServer { this: MillBuildSer
112112
targetIds = _ => p.getTargets.asScala.toSeq,
113113
tasks = {
114114
case m: TestModule =>
115-
T.task(Some((m.runClasspath(), m.testFramework(), m.compile())))
115+
T.task(Some((m.runClasspath(), m.testFramework(), m.testClasspath())))
116116
case _ =>
117117
T.task(None)
118118
}
119119
) {
120-
case (ev, state, id, m: TestModule, Some((classpath, testFramework, compResult))) =>
120+
case (ev, state, id, m: TestModule, Some((classpath, testFramework, testClasspath))) =>
121121
val (frameworkName, classFingerprint): (String, Agg[(Class[_], Fingerprint)]) =
122122
Jvm.inprocess(
123123
classpath.map(_.path),
@@ -129,7 +129,7 @@ private trait MillScalaBuildServer extends ScalaBuildServer { this: MillBuildSer
129129
val discoveredTests = TestRunnerUtils.discoverTests(
130130
cl,
131131
framework,
132-
Agg(compResult.classes.path)
132+
Agg.from(testClasspath.map(_.path))
133133
)
134134
(framework.name(), discoveredTests)
135135
}

build.sc

+27-6
Original file line numberDiff line numberDiff line change
@@ -428,28 +428,48 @@ trait MillPublishScalaModule extends MillScalaModule with MillPublishJavaModule
428428
trait MillStableScalaModule extends MillPublishScalaModule with Mima {
429429
import com.github.lolgab.mill.mima._
430430
override def mimaBinaryIssueFilters: T[Seq[ProblemFilter]] = Seq(
431-
// MIMA doesn't properly ignore things which are nested inside other private things
431+
// (5x) MIMA doesn't properly ignore things which are nested inside other private things
432432
// so we have to put explicit ignores here (https://github.com/lightbend/mima/issues/771)
433433
ProblemFilter.exclude[Problem]("mill.eval.ProfileLogger*"),
434434
ProblemFilter.exclude[Problem]("mill.eval.GroupEvaluator*"),
435435
ProblemFilter.exclude[Problem]("mill.eval.Tarjans*"),
436436
ProblemFilter.exclude[Problem]("mill.define.Ctx#Impl*"),
437437
ProblemFilter.exclude[Problem]("mill.resolve.ResolveNotFoundHandler*"),
438-
// See https://github.com/com-lihaoyi/mill/pull/2739
438+
// (4x) See https://github.com/com-lihaoyi/mill/pull/2739
439439
ProblemFilter.exclude[ReversedMissingMethodProblem](
440440
"mill.scalajslib.ScalaJSModule.mill$scalajslib$ScalaJSModule$$super$scalaLibraryIvyDeps"
441441
),
442-
// See https://github.com/com-lihaoyi/mill/pull/3072
443442
ProblemFilter.exclude[ReversedMissingMethodProblem](
444443
"mill.scalalib.ScalaModule.mill$scalalib$ScalaModule$$super$zincAuxiliaryClassFileExtensions"
445444
),
446-
// See https://github.com/com-lihaoyi/mill/pull/3072
447445
ProblemFilter.exclude[ReversedMissingMethodProblem](
448446
"mill.scalajslib.ScalaJSModule.mill$scalajslib$ScalaJSModule$$super$zincAuxiliaryClassFileExtensions"
449447
),
450-
// See https://github.com/com-lihaoyi/mill/pull/3072
451448
ProblemFilter.exclude[ReversedMissingMethodProblem](
452449
"mill.scalanativelib.ScalaNativeModule.mill$scalanativelib$ScalaNativeModule$$super$zincAuxiliaryClassFileExtensions"
450+
),
451+
// (7x) See https://github.com/com-lihaoyi/mill/pull/3064
452+
// Moved targets up in trait hierarchy, but also call them via super, which I think is safe
453+
ProblemFilter.exclude[ReversedMissingMethodProblem](
454+
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$zincWorker"
455+
),
456+
ProblemFilter.exclude[ReversedMissingMethodProblem](
457+
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$runClasspath"
458+
),
459+
ProblemFilter.exclude[ReversedMissingMethodProblem](
460+
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$runUseArgsFile"
461+
),
462+
ProblemFilter.exclude[ReversedMissingMethodProblem](
463+
"mill.scalalib.JavaModule#JavaModuleTests.mill$scalalib$JavaModule$JavaModuleTests$$super$testClasspath"
464+
),
465+
ProblemFilter.exclude[ReversedMissingMethodProblem](
466+
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$forkArgs"
467+
),
468+
ProblemFilter.exclude[ReversedMissingMethodProblem](
469+
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$forkEnv"
470+
),
471+
ProblemFilter.exclude[ReversedMissingMethodProblem](
472+
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$forkWorkingDir"
453473
)
454474
)
455475
def mimaPreviousVersions: T[Seq[String]] = Settings.mimaBaseVersions
@@ -975,7 +995,8 @@ object contrib extends Module {
975995

976996
object scalanativelib extends MillStableScalaModule {
977997
def moduleDeps = Seq(scalalib, scalanativelib.`worker-api`)
978-
def testTransitiveDeps = super.testTransitiveDeps() ++ Seq(worker("0.4").testDep(), worker("0.5").testDep())
998+
def testTransitiveDeps =
999+
super.testTransitiveDeps() ++ Seq(worker("0.4").testDep(), worker("0.5").testDep())
9791000

9801001
object `worker-api` extends MillPublishScalaModule {
9811002
def ivyDeps = Agg(Deps.sbtTestInterface)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import mill._
2+
import mill.scalalib._
3+
import mill.scalalib.api.CompilationResult
4+
5+
object foo extends RootModule with ScalaModule {
6+
def scalaVersion = "2.13.11"
7+
def ivyDeps = Agg(
8+
ivy"com.lihaoyi::scalatags:0.12.0",
9+
ivy"com.lihaoyi::mainargs:0.6.2"
10+
)
11+
12+
object test extends ScalaTests {
13+
def ivyDeps = Agg(
14+
ivy"com.lihaoyi::utest:0.7.11",
15+
ivy"org.scalatest::scalatest-freespec:3.2.18"
16+
)
17+
def testFramework = "utest.runner.Framework"
18+
}
19+
object test2 extends TestModule with TestModule.ScalaTest {
20+
override def compile: T[CompilationResult] = ???
21+
override def runClasspath: T[Seq[PathRef]] = foo.test.runClasspath()
22+
override def testClasspath = foo.test.testClasspath()
23+
}
24+
}
25+
26+
// format: off
27+
/** Usage
28+
29+
> mill resolve __:TestModule.test
30+
...
31+
test.test
32+
test2.test
33+
34+
> mill test
35+
...
36+
+ foo.FooTests.simple ... <h1>hello</h1>
37+
+ foo.FooTests.escaping ... <h1>&lt;hello&gt;</h1>
38+
Tests: 2, Passed: 2, Failed: 0
39+
> mill test2
40+
...
41+
FooScalaTests:
42+
Foo
43+
- simple
44+
- escaping
45+
...
46+
Total number of tests run: 2
47+
Suites: completed 1, aborted 0
48+
Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
49+
50+
*/
51+
// format: on
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package foo
2+
import scalatags.Text.all._
3+
import mainargs.{main, ParserForMethods}
4+
5+
object Foo {
6+
def generateHtml(text: String) = {
7+
h1(text).toString
8+
}
9+
10+
@main
11+
def main(text: String) = {
12+
println(generateHtml(text))
13+
}
14+
15+
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package foo
2+
3+
import org.scalatest.freespec._
4+
5+
class FooScalaTests extends AnyFreeSpec {
6+
"Foo" - {
7+
"simple" in {
8+
val result = Foo.generateHtml("hello")
9+
assert(result == "<h1>hello</h1>")
10+
result
11+
}
12+
"escaping" in {
13+
val result = Foo.generateHtml("<hello>")
14+
assert(result == "<h1>&lt;hello&gt;</h1>")
15+
result
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package foo
2+
import utest._
3+
object FooTests extends TestSuite {
4+
def tests = Tests {
5+
test("simple") {
6+
val result = Foo.generateHtml("hello")
7+
assert(result == "<h1>hello</h1>")
8+
result
9+
}
10+
test("escaping") {
11+
val result = Foo.generateHtml("<hello>")
12+
assert(result == "<h1>&lt;hello&gt;</h1>")
13+
result
14+
}
15+
}
16+
}

example/src/mill/integration/ExampleTestSuite.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import mill.integration.{BashTokenizer, IntegrationTestSuite}
33
import utest._
44
import mill.util.Util
55

6+
import scala.util.chaining.scalaUtilChainingOps
7+
68
/**
79
* Shared implementation for the tests in `example/`.
810
*
@@ -85,7 +87,15 @@ object ExampleTestSuite extends IntegrationTestSuite {
8587
expectedSnippets: Vector[String],
8688
commandStr: String
8789
): Unit = {
88-
BashTokenizer.tokenize(commandStr) match {
90+
BashTokenizer.tokenize(commandStr)
91+
.tap { cmd =>
92+
Console.err.println(
93+
s"""$workspaceRoot> ${cmd.mkString("'", "' '", "'")}
94+
|--- Expected output --------
95+
|${expectedSnippets.mkString("\n")}
96+
|----------------------------""".stripMargin
97+
)
98+
} match {
8999
case Seq("cp", "-r", from, to) =>
90100
os.copy(os.Path(from, workspaceRoot), os.Path(to, workspaceRoot))
91101

scalalib/src/mill/scalalib/JavaModule.scala

+41-10
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,17 @@ import os.{Path, ProcessOutput}
2323
*/
2424
trait JavaModule
2525
extends mill.Module
26+
with WithZincWorker
27+
with TestModule.JavaModuleBase
2628
with TaskModule
29+
with RunModule
2730
with GenIdeaModule
2831
with CoursierModule
2932
with OfflineSupportModule
3033
with BspModule
31-
with SemanticDbJavaModule
32-
with TestModule.JavaModuleBase { outer =>
34+
with SemanticDbJavaModule { outer =>
3335

34-
def zincWorker: ModuleRef[ZincWorkerModule] = ModuleRef(mill.scalalib.ZincWorkerModule)
36+
def zincWorker: ModuleRef[ZincWorkerModule] = super.zincWorker
3537

3638
trait JavaModuleTests extends JavaModule with TestModule {
3739
// Run some consistence checks
@@ -51,6 +53,15 @@ trait JavaModule
5153
}
5254
}
5355

56+
/**
57+
* The classpath containing the tests. This defaults to the compilation output.
58+
*/
59+
def testClasspath: T[Seq[PathRef]] = T {
60+
// bin-compat-shim: keep the super.call in the classfile
61+
super.testClasspath
62+
Seq(compile().classes)
63+
}
64+
5465
/**
5566
* JavaModule and its derivates define inner test modules.
5667
* To avoid unexpected misbehavior due to the use of the wrong inner test trait
@@ -155,11 +166,17 @@ trait JavaModule
155166

156167
/**
157168
* The direct dependencies of this module.
169+
* This is meant to be overridden to add dependencies.
170+
* To read the value, you should use [[moduleDepsChecked]] instead,
171+
* which uses a cached result which is also checked to be free of cycle.
158172
* @see [[moduleDepschecked]]
159173
*/
160174
def moduleDeps: Seq[JavaModule] = Seq.empty
161175

162-
/** Same as [[moduleDeps]] but checked to not contain cycles. */
176+
/**
177+
* Same as [[moduleDeps]] but checked to not contain cycles.
178+
* Prefer this over using [[moduleDeps]] directly.
179+
*/
163180
final def moduleDepsChecked: Seq[JavaModule] = {
164181
// trigger initialization to check for cycles
165182
recModuleDeps
@@ -517,7 +534,10 @@ trait JavaModule
517534
* necessary to run this module's code after compilation
518535
*/
519536
def runClasspath: T[Seq[PathRef]] = T {
520-
resolvedRunIvyDeps().toSeq ++ transitiveLocalClasspath() ++ localClasspath()
537+
super.runClasspath() ++
538+
resolvedRunIvyDeps().toSeq ++
539+
transitiveLocalClasspath() ++
540+
localClasspath()
521541
}
522542

523543
/**
@@ -668,13 +688,19 @@ trait JavaModule
668688
* Any command-line parameters you want to pass to the forked JVM under `run`,
669689
* `test` or `repl`
670690
*/
671-
def forkArgs: T[Seq[String]] = T { Seq.empty[String] }
691+
def forkArgs: T[Seq[String]] = T {
692+
// overridden here for binary compatibility (0.11.x)
693+
super.forkArgs()
694+
}
672695

673696
/**
674697
* Any environment variables you want to pass to the forked JVM under `run`,
675698
* `test` or `repl`
676699
*/
677-
def forkEnv: T[Map[String, String]] = T.input { T.env }
700+
def forkEnv: T[Map[String, String]] = T {
701+
// overridden here for binary compatibility (0.11.x)
702+
super.forkEnv()
703+
}
678704

679705
/**
680706
* Builds a command-line "launcher" file that can be used to run this module's
@@ -789,8 +815,10 @@ trait JavaModule
789815
}
790816
}
791817

792-
/** Control whether `run*`-targets should use an args file to pass command line args, if possible. */
793-
def runUseArgsFile: T[Boolean] = T { scala.util.Properties.isWin }
818+
def runUseArgsFile: T[Boolean] = T {
819+
// overridden here for binary compatibility (0.11.x)
820+
super.runUseArgsFile()
821+
}
794822

795823
/**
796824
* Runs this module's code in-process within an isolated classloader. This is
@@ -1004,7 +1032,10 @@ trait JavaModule
10041032
*/
10051033
def artifactSuffix: T[String] = platformSuffix()
10061034

1007-
def forkWorkingDir: T[Path] = T { T.workspace }
1035+
def forkWorkingDir: T[Path] = T {
1036+
// overridden here for binary compatibility (0.11.x)
1037+
super.forkWorkingDir()
1038+
}
10081039

10091040
/**
10101041
* Files extensions that need to be managed by Zinc together with class files.
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package mill.scalalib
2+
3+
import mill.T
4+
import mill.define.Module
5+
import mill.api.JsonFormatters.pathReadWrite
6+
import mill.api.PathRef
7+
8+
trait RunModule extends Module {
9+
10+
/**
11+
* Any command-line parameters you want to pass to the forked JVM.
12+
*/
13+
def forkArgs: T[Seq[String]] = T { Seq.empty[String] }
14+
15+
/**
16+
* Any environment variables you want to pass to the forked JVM.
17+
*/
18+
def forkEnv: T[Map[String, String]] = T.input { T.env }
19+
20+
def forkWorkingDir: T[os.Path] = T { T.workspace }
21+
22+
/**
23+
* All classfiles and resources including upstream modules and dependencies
24+
* necessary to run this module's code.
25+
*/
26+
def runClasspath: T[Seq[PathRef]] = T { Seq.empty[PathRef] }
27+
28+
/**
29+
* Control whether `run*`-targets should use an args file to pass command line args, if possible.
30+
*/
31+
def runUseArgsFile: T[Boolean] = T { scala.util.Properties.isWin }
32+
33+
// def zincWorker: ModuleRef[ZincWorkerModule] = ModuleRef(mill.scalalib.ZincWorkerModule)
34+
35+
}

0 commit comments

Comments
 (0)