Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure TestModule, add RunModule #3064

Merged
merged 15 commits into from
Mar 20, 2024
6 changes: 3 additions & 3 deletions bsp/worker/src/mill/bsp/worker/MillScalaBuildServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,12 @@ private trait MillScalaBuildServer extends ScalaBuildServer { this: MillBuildSer
targetIds = _ => p.getTargets.asScala.toSeq,
tasks = {
case m: TestModule =>
T.task(Some((m.runClasspath(), m.testFramework(), m.compile())))
T.task(Some((m.runClasspath(), m.testFramework(), m.testClasspath())))
case _ =>
T.task(None)
}
) {
case (ev, state, id, m: TestModule, Some((classpath, testFramework, compResult))) =>
case (ev, state, id, m: TestModule, Some((classpath, testFramework, testClasspath))) =>
val (frameworkName, classFingerprint): (String, Agg[(Class[_], Fingerprint)]) =
Jvm.inprocess(
classpath.map(_.path),
Expand All @@ -129,7 +129,7 @@ private trait MillScalaBuildServer extends ScalaBuildServer { this: MillBuildSer
val discoveredTests = TestRunnerUtils.discoverTests(
cl,
framework,
Agg(compResult.classes.path)
Agg.from(testClasspath.map(_.path))
)
(framework.name(), discoveredTests)
}
Expand Down
33 changes: 27 additions & 6 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -428,28 +428,48 @@ trait MillPublishScalaModule extends MillScalaModule with MillPublishJavaModule
trait MillStableScalaModule extends MillPublishScalaModule with Mima {
import com.github.lolgab.mill.mima._
override def mimaBinaryIssueFilters: T[Seq[ProblemFilter]] = Seq(
// MIMA doesn't properly ignore things which are nested inside other private things
// (5x) MIMA doesn't properly ignore things which are nested inside other private things
// so we have to put explicit ignores here (https://github.com/lightbend/mima/issues/771)
ProblemFilter.exclude[Problem]("mill.eval.ProfileLogger*"),
ProblemFilter.exclude[Problem]("mill.eval.GroupEvaluator*"),
ProblemFilter.exclude[Problem]("mill.eval.Tarjans*"),
ProblemFilter.exclude[Problem]("mill.define.Ctx#Impl*"),
ProblemFilter.exclude[Problem]("mill.resolve.ResolveNotFoundHandler*"),
// See https://github.com/com-lihaoyi/mill/pull/2739
// (4x) See https://github.com/com-lihaoyi/mill/pull/2739
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalajslib.ScalaJSModule.mill$scalajslib$ScalaJSModule$$super$scalaLibraryIvyDeps"
),
// See https://github.com/com-lihaoyi/mill/pull/3072
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.ScalaModule.mill$scalalib$ScalaModule$$super$zincAuxiliaryClassFileExtensions"
),
// See https://github.com/com-lihaoyi/mill/pull/3072
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalajslib.ScalaJSModule.mill$scalajslib$ScalaJSModule$$super$zincAuxiliaryClassFileExtensions"
),
// See https://github.com/com-lihaoyi/mill/pull/3072
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalanativelib.ScalaNativeModule.mill$scalanativelib$ScalaNativeModule$$super$zincAuxiliaryClassFileExtensions"
),
// (7x) See https://github.com/com-lihaoyi/mill/pull/3064
// Moved targets up in trait hierarchy, but also call them via super, which I think is safe
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$zincWorker"
),
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$runClasspath"
),
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$runUseArgsFile"
),
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule#JavaModuleTests.mill$scalalib$JavaModule$JavaModuleTests$$super$testClasspath"
),
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$forkArgs"
),
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$forkEnv"
),
ProblemFilter.exclude[ReversedMissingMethodProblem](
"mill.scalalib.JavaModule.mill$scalalib$JavaModule$$super$forkWorkingDir"
)
)
def mimaPreviousVersions: T[Seq[String]] = Settings.mimaBaseVersions
Expand Down Expand Up @@ -975,7 +995,8 @@ object contrib extends Module {

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

object `worker-api` extends MillPublishScalaModule {
def ivyDeps = Agg(Deps.sbtTestInterface)
Expand Down
51 changes: 51 additions & 0 deletions example/basic/5-multiple-test-frameworks/build.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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()
}
}

// format: off
/** Usage

> mill resolve __:TestModule.test
...
test.test
test2.test

> mill test
...
+ foo.FooTests.simple ... <h1>hello</h1>
+ foo.FooTests.escaping ... <h1>&lt;hello&gt;</h1>
Tests: 2, Passed: 2, Failed: 0
> mill test2
...
FooScalaTests:
Foo
- simple
- escaping
...
Total number of tests run: 2
Suites: completed 1, aborted 0
Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0

*/
// format: on
16 changes: 16 additions & 0 deletions example/basic/5-multiple-test-frameworks/src/Foo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo
import scalatags.Text.all._
import mainargs.{main, ParserForMethods}

object Foo {
def generateHtml(text: String) = {
h1(text).toString
}

@main
def main(text: String) = {
println(generateHtml(text))
}

def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package foo

import org.scalatest.freespec._

class FooScalaTests extends AnyFreeSpec {
"Foo" - {
"simple" in {
val result = Foo.generateHtml("hello")
assert(result == "<h1>hello</h1>")
result
}
"escaping" in {
val result = Foo.generateHtml("<hello>")
assert(result == "<h1>&lt;hello&gt;</h1>")
result
}
}
}
16 changes: 16 additions & 0 deletions example/basic/5-multiple-test-frameworks/test/src/FooTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo
import utest._
object FooTests extends TestSuite {
def tests = Tests {
test("simple") {
val result = Foo.generateHtml("hello")
assert(result == "<h1>hello</h1>")
result
}
test("escaping") {
val result = Foo.generateHtml("<hello>")
assert(result == "<h1>&lt;hello&gt;</h1>")
result
}
}
}
12 changes: 11 additions & 1 deletion example/src/mill/integration/ExampleTestSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import mill.integration.{BashTokenizer, IntegrationTestSuite}
import utest._
import mill.util.Util

import scala.util.chaining.scalaUtilChainingOps

/**
* Shared implementation for the tests in `example/`.
*
Expand Down Expand Up @@ -85,7 +87,15 @@ object ExampleTestSuite extends IntegrationTestSuite {
expectedSnippets: Vector[String],
commandStr: String
): Unit = {
BashTokenizer.tokenize(commandStr) match {
BashTokenizer.tokenize(commandStr)
.tap { cmd =>
Console.err.println(
s"""$workspaceRoot> ${cmd.mkString("'", "' '", "'")}
|--- Expected output --------
|${expectedSnippets.mkString("\n")}
|----------------------------""".stripMargin
)
} match {
case Seq("cp", "-r", from, to) =>
os.copy(os.Path(from, workspaceRoot), os.Path(to, workspaceRoot))

Expand Down
51 changes: 41 additions & 10 deletions scalalib/src/mill/scalalib/JavaModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ import os.{Path, ProcessOutput}
*/
trait JavaModule
extends mill.Module
with WithZincWorker
with TestModule.JavaModuleBase
with TaskModule
with RunModule
with GenIdeaModule
with CoursierModule
with OfflineSupportModule
with BspModule
with SemanticDbJavaModule
with TestModule.JavaModuleBase { outer =>
with SemanticDbJavaModule { outer =>

def zincWorker: ModuleRef[ZincWorkerModule] = ModuleRef(mill.scalalib.ZincWorkerModule)
def zincWorker: ModuleRef[ZincWorkerModule] = super.zincWorker

trait JavaModuleTests extends JavaModule with TestModule {
// Run some consistence checks
Expand All @@ -51,6 +53,15 @@ trait JavaModule
}
}

/**
* The classpath containing the tests. This defaults to the compilation output.
*/
def testClasspath: T[Seq[PathRef]] = T {
// bin-compat-shim: keep the super.call in the classfile
super.testClasspath
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling super here but without calling apply, so even when not actual using the super-target here (we don't insert a real dependency to the super-target), we ensure the super-reference is generated in the byte-code. This allows us later to use the super-target without worrying about binary compatibility.

This is some new pattern I want to try out and establish. If it works out, we could automate it in the T-macros.

See for more details:

Seq(compile().classes)
}

/**
* JavaModule and its derivates define inner test modules.
* To avoid unexpected misbehavior due to the use of the wrong inner test trait
Expand Down Expand Up @@ -155,11 +166,17 @@ trait JavaModule

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

/** Same as [[moduleDeps]] but checked to not contain cycles. */
/**
* Same as [[moduleDeps]] but checked to not contain cycles.
* Prefer this over using [[moduleDeps]] directly.
*/
final def moduleDepsChecked: Seq[JavaModule] = {
// trigger initialization to check for cycles
recModuleDeps
Expand Down Expand Up @@ -517,7 +534,10 @@ trait JavaModule
* necessary to run this module's code after compilation
*/
def runClasspath: T[Seq[PathRef]] = T {
resolvedRunIvyDeps().toSeq ++ transitiveLocalClasspath() ++ localClasspath()
super.runClasspath() ++
resolvedRunIvyDeps().toSeq ++
transitiveLocalClasspath() ++
localClasspath()
}

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

/**
* Any environment variables you want to pass to the forked JVM under `run`,
* `test` or `repl`
*/
def forkEnv: T[Map[String, String]] = T.input { T.env }
def forkEnv: T[Map[String, String]] = T {
// overridden here for binary compatibility (0.11.x)
super.forkEnv()
}

/**
* Builds a command-line "launcher" file that can be used to run this module's
Expand Down Expand Up @@ -789,8 +815,10 @@ trait JavaModule
}
}

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

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

def forkWorkingDir: T[Path] = T { T.workspace }
def forkWorkingDir: T[Path] = T {
// overridden here for binary compatibility (0.11.x)
super.forkWorkingDir()
}

/**
* Files extensions that need to be managed by Zinc together with class files.
Expand Down
35 changes: 35 additions & 0 deletions scalalib/src/mill/scalalib/RunModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package mill.scalalib

import mill.T
import mill.define.Module
import mill.api.JsonFormatters.pathReadWrite
import mill.api.PathRef

trait RunModule extends Module {

/**
* Any command-line parameters you want to pass to the forked JVM.
*/
def forkArgs: T[Seq[String]] = T { Seq.empty[String] }

/**
* Any environment variables you want to pass to the forked JVM.
*/
def forkEnv: T[Map[String, String]] = T.input { T.env }

def forkWorkingDir: T[os.Path] = T { T.workspace }

/**
* All classfiles and resources including upstream modules and dependencies
* necessary to run this module's code.
*/
def runClasspath: T[Seq[PathRef]] = T { Seq.empty[PathRef] }

/**
* Control whether `run*`-targets should use an args file to pass command line args, if possible.
*/
def runUseArgsFile: T[Boolean] = T { scala.util.Properties.isWin }

// def zincWorker: ModuleRef[ZincWorkerModule] = ModuleRef(mill.scalalib.ZincWorkerModule)

}
Loading