Skip to content

Commit 712d459

Browse files
chore: improve mill tests (#2049)
1 parent a07d294 commit 712d459

6 files changed

Lines changed: 332 additions & 13 deletions

File tree

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ lazy val sbtPlugin = (projectMatrix in file("modules") / "sbt")
5757
.enablePlugins(SbtPlugin)
5858
.defaultAxes(VirtualAxis.scalaPartialVersion("2.12"), VirtualAxis.jvm)
5959
.settings(commonSettings, sbtPluginSettings, publishLocalDependsOn(core, testRunner))
60-
.dependsOn(core)
60+
.dependsOn(core, testkit % Test)
6161
.jvmPlatform(scalaVersions = Seq(versions.scala3, versions.scala212))
6262

6363
// Mill plugins are compiled with the Scala version of the minimum supported Mill version
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package mill.api.daemon {
2+
3+
import java.io.{ByteArrayInputStream, PrintStream}
4+
import scala.collection.mutable.ListBuffer
5+
6+
/** A test double for Mill's [[Logger]]. Lives in `mill.api.daemon` because `prompt` (which backs `debugEnabled` and
7+
* color detection) is `private[mill]` and can only be overridden from within the `mill` package.
8+
*/
9+
class FakeMillLogger(debugOn: Boolean, coloredOn: Boolean) extends Logger {
10+
val logged: ListBuffer[(String, String)] = ListBuffer.empty
11+
12+
override val streams: SystemStreams = new SystemStreams(
13+
new PrintStream(_ => ()),
14+
new PrintStream(_ => ()),
15+
new ByteArrayInputStream(Array.empty)
16+
)
17+
18+
override def info(s: String): Unit = logged += (("info", s))
19+
override def debug(s: String): Unit = logged += (("debug", s))
20+
override def warn(s: String): Unit = logged += (("warn", s))
21+
override def error(s: String): Unit = logged += (("error", s))
22+
override def ticker(s: String): Unit = logged += (("ticker", s))
23+
24+
override private[mill] def prompt: Logger.Prompt = new Logger.Prompt.NoOp {
25+
override def debugEnabled: Boolean = debugOn
26+
override def colored: Boolean = coloredOn
27+
}
28+
}
29+
}
30+
31+
package stryker4s.log {
32+
33+
import mill.api.daemon.FakeMillLogger
34+
import stryker4s.testkit.Stryker4sSuite
35+
36+
class MillLoggerTest extends Stryker4sSuite {
37+
38+
/** Exposes the `protected` `colorEnabled` for assertions. */
39+
private class ProbeLogger(colored: Boolean, env: Map[String, String])
40+
extends MillLogger(new FakeMillLogger(debugOn = false, coloredOn = colored), env) {
41+
def color: Boolean = colorEnabled
42+
}
43+
44+
private def colorOf(colored: Boolean, env: Map[String, String]): Boolean =
45+
new ProbeLogger(colored, env).color
46+
47+
test("debug messages are only logged when mill has debug enabled") {
48+
val enabled = new FakeMillLogger(debugOn = true, coloredOn = false)
49+
new MillLogger(enabled, Map.empty).log(Level.Debug, "debug message")
50+
assertEquals(enabled.logged.toList, List("debug" -> "debug message"))
51+
52+
val disabled = new FakeMillLogger(debugOn = false, coloredOn = false)
53+
new MillLogger(disabled, Map.empty).log(Level.Debug, "debug message")
54+
assertEquals(disabled.logged.toList, List.empty)
55+
}
56+
57+
test("info, warn and error are always forwarded to mill") {
58+
val millLogger = new FakeMillLogger(debugOn = false, coloredOn = false)
59+
val logger = new MillLogger(millLogger, Map.empty)
60+
logger.log(Level.Info, "an info")
61+
logger.log(Level.Warn, "a warning")
62+
logger.log(Level.Error, "an error")
63+
assertEquals(
64+
millLogger.logged.toList,
65+
List("info" -> "an info", "warn" -> "a warning", "error" -> "an error")
66+
)
67+
}
68+
69+
test("color is enabled when mill enables color and NO_COLOR is unset") {
70+
assert(colorOf(colored = true, env = Map.empty))
71+
}
72+
73+
test("color is disabled when NO_COLOR is set, even if mill enables color") {
74+
assert(!colorOf(colored = true, env = Map("NO_COLOR" -> "")))
75+
}
76+
77+
test("color is disabled when mill disables color, even if NO_COLOR is unset") {
78+
assert(!colorOf(colored = false, env = Map.empty))
79+
}
80+
}
81+
}

modules/mill/src/test/scala/stryker4s/mill/MillConfigSourceTest.scala

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package stryker4s.mill
22

33
import cats.effect.IO
4+
import cats.syntax.all.*
5+
import ciris.ConfigValue
46
import fs2.io.file.Path
7+
import stryker4s.config.*
58
import stryker4s.testkit.Stryker4sIOSuite
9+
import sttp.client4.UriContext
610

711
import scala.concurrent.duration.*
812
import scala.meta.dialects
@@ -40,15 +44,15 @@ class MillConfigSourceTest extends Stryker4sIOSuite {
4044
val config = new MillConfigSource[IO](
4145
baseDirValue = Path("/tmp/project"),
4246
mutateValue = Some(Seq("src/main/scala/**.scala")),
43-
filesValue = None,
47+
filesValue = Some(Seq("src/main/scala/**")),
4448
testFilterValue = Some(Seq("*MyTest")),
4549
reportersValue = Some(Seq("html", "console")),
4650
excludedMutationsValue = Some(Seq("BooleanLiteral")),
4751
thresholdsHighValue = Some(85),
4852
thresholdsLowValue = Some(60),
4953
thresholdsBreakValue = Some(0),
50-
dashboardBaseUrlValue = None,
51-
dashboardReportTypeValue = None,
54+
dashboardBaseUrlValue = Some("https://dashboard.stryker-mutator.io"),
55+
dashboardReportTypeValue = Some("full"),
5256
dashboardProjectValue = Some("github.com/foo/bar"),
5357
dashboardVersionValue = Some("main"),
5458
dashboardModuleValue = Some("my-module"),
@@ -66,24 +70,69 @@ class MillConfigSourceTest extends Stryker4sIOSuite {
6670

6771
for {
6872
_ <- config.mutate.load.assertEquals(Seq("src/main/scala/**.scala"))
73+
_ <- config.files.load.assertEquals(Seq("src/main/scala/**"))
6974
_ <- config.testFilter.load.assertEquals(Seq("*MyTest"))
75+
_ <- config.reporters.load.assertEquals(Seq[ReporterType](Html, Console))
76+
_ <- config.excludedMutations.load.assertEquals(Seq(ExcludedMutation("BooleanLiteral")))
7077
_ <- config.thresholdsHigh.load.assertEquals(85)
78+
_ <- config.thresholdsLow.load.assertEquals(60)
79+
_ <- config.thresholdsBreak.load.assertEquals(0)
80+
_ <- config.dashboardBaseUrl.load.assertEquals(uri"https://dashboard.stryker-mutator.io")
81+
_ <- config.dashboardReportType.load.assertEquals(Full: DashboardReportType)
7182
_ <- config.dashboardProject.load.assertEquals(Some("github.com/foo/bar"))
83+
_ <- config.dashboardVersion.load.assertEquals(Some("main"))
7284
_ <- config.dashboardModule.load.assertEquals(Some("my-module"))
7385
_ <- config.timeout.load.assertEquals(5.seconds)
86+
_ <- config.timeoutFactor.load.assertEquals(1.5)
87+
_ <- config.maxTestRunnerReuse.load.assertEquals(Some(10))
88+
_ <- config.scalaDialect.load.assertEquals(dialects.Scala3)
7489
_ <- config.concurrency.load.assertEquals(4)
90+
_ <- config.debugLogTestRunnerStdout.load.assertEquals(true)
91+
_ <- config.debugDebugTestRunner.load.assertEquals(false)
92+
_ <- config.staticTmpDir.load.assertEquals(false)
7593
_ <- config.cleanTmpDir.load.assertEquals(true)
94+
_ <- config.openReport.load.assertEquals(false)
7695
} yield ()
7796
}
7897

7998
test("should make the baseDir absolute") {
8099
emptyConfigSource.baseDir.load.assertEquals(Path("/tmp/project").absolute)
81100
}
82101

83-
test("missing values are reported as missing") {
84-
emptyConfigSource.thresholdsHigh.attempt
85-
.map(_.leftValue.messages.loneElement)
86-
.assertEquals("Missing mill config strykerThresholdsHigh")
102+
test("missing values are reported as missing with their mill config key") {
103+
val source = emptyConfigSource
104+
// Every value-backed config def should report the exact mill build key it reads from when absent
105+
val cases: List[(ConfigValue[IO, ?], String)] = List(
106+
source.mutate -> "strykerMutate",
107+
source.files -> "strykerFiles",
108+
source.testFilter -> "strykerTestFilter",
109+
source.reporters -> "strykerReporters",
110+
source.excludedMutations -> "strykerExcludedMutations",
111+
source.thresholdsHigh -> "strykerThresholdsHigh",
112+
source.thresholdsLow -> "strykerThresholdsLow",
113+
source.thresholdsBreak -> "strykerThresholdsBreak",
114+
source.dashboardBaseUrl -> "strykerDashboardBaseUrl",
115+
source.dashboardReportType -> "strykerDashboardReportType",
116+
source.dashboardProject -> "strykerDashboardProject",
117+
source.dashboardVersion -> "strykerDashboardVersion",
118+
source.dashboardModule -> "strykerDashboardModule",
119+
source.timeout -> "strykerTimeout",
120+
source.timeoutFactor -> "strykerTimeoutFactor",
121+
source.maxTestRunnerReuse -> "strykerMaxTestRunnerReuse",
122+
source.scalaDialect -> "strykerScalaDialect",
123+
source.concurrency -> "strykerConcurrency",
124+
source.debugLogTestRunnerStdout -> "strykerDebugLogTestRunnerStdout",
125+
source.debugDebugTestRunner -> "strykerDebugDebugTestRunner",
126+
source.staticTmpDir -> "strykerStaticTmpDir",
127+
source.cleanTmpDir -> "strykerCleanTmpDir",
128+
source.openReport -> "strykerOpenReport"
129+
)
130+
131+
cases.traverse_ { case (configValue, key) =>
132+
configValue.attempt
133+
.map(_.leftValue.messages.loneElement)
134+
.assertEquals(s"Missing mill config $key")
135+
}
87136
}
88137

89138
test("fails on values not supported by the mill plugin") {

modules/mill/src/test/scala/stryker4s/mill/StrykerModuleTest.scala

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import mill.util.TokenReaders.*
77
import stryker4s.testkit.Stryker4sSuite
88

99
import java.nio.file.Paths
10+
import scala.meta.dialects
1011

1112
class Stryker4sModuleTest extends Stryker4sSuite {
1213

@@ -50,6 +51,29 @@ class Stryker4sModuleTest extends Stryker4sSuite {
5051
lazy val millDiscover = Discover[this.type]
5152
}
5253

54+
object scala2Project extends TestRootModule {
55+
// Scala 2.13 module with `-Xsource:3` enabled, plus an unrelated option to distinguish exists/forall
56+
object source3 extends ScalaModule, Stryker4sModule {
57+
def scalaVersion = "2.13.18"
58+
override def scalacOptions = Seq("-Xsource:3", "-deprecation")
59+
60+
object test extends ScalaTests {
61+
def testFramework = "munit.Framework"
62+
}
63+
}
64+
// Scala 2.13 module without `-Xsource:3`
65+
object plain extends ScalaModule, Stryker4sModule {
66+
def scalaVersion = "2.13.18"
67+
override def scalacOptions = Seq("-deprecation")
68+
69+
object test extends ScalaTests {
70+
def testFramework = "munit.Framework"
71+
}
72+
}
73+
74+
lazy val millDiscover = Discover[this.type]
75+
}
76+
5377
test("strykerTestModule resolves the child test module") {
5478
UnitTester(unitTestProject, resourceProject("unit-test-project")).scoped { _ =>
5579
assertEquals(unitTestProject.foo.strykerTestModule, unitTestProject.foo.test)
@@ -81,4 +105,30 @@ class Stryker4sModuleTest extends Stryker4sSuite {
81105
)
82106
}
83107
}
108+
109+
test("strykerMutate and strykerFiles produce globs relative to the module dir") {
110+
UnitTester(unitTestProject, resourceProject("unit-test-project")).scoped { eval =>
111+
assertEquals(eval(unitTestProject.foo.strykerMutate).value.value, Some(Seq("src/**.scala")))
112+
assertEquals(eval(unitTestProject.foo.strykerFiles).value.value, Some(Seq("src/**")))
113+
}
114+
}
115+
116+
test("strykerScalaDialect derives the scalameta dialect from the scala version") {
117+
UnitTester(unitTestProject, resourceProject("unit-test-project")).scoped { eval =>
118+
// scalaVersion is 3.3.8, so the dialect should be Scala 3.3
119+
assertEquals(eval(unitTestProject.foo.strykerScalaDialect).value.value, Some(dialects.Scala33))
120+
}
121+
}
122+
123+
test("strykerScalaDialect picks the source3 dialect for Scala 2 with -Xsource:3") {
124+
UnitTester(scala2Project, resourceProject("unit-test-project")).scoped { eval =>
125+
assertEquals(eval(scala2Project.source3.strykerScalaDialect).value.value, Some(dialects.Scala213Source3))
126+
}
127+
}
128+
129+
test("strykerScalaDialect picks the plain dialect for Scala 2 without -Xsource:3") {
130+
UnitTester(scala2Project, resourceProject("unit-test-project")).scoped { eval =>
131+
assertEquals(eval(scala2Project.plain.strykerScalaDialect).value.value, Some(dialects.Scala213))
132+
}
133+
}
84134
}

modules/mill/src/test/scala/stryker4s/mill/runner/MillTestDiscoveryTest.scala

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,23 @@ class MillTestDiscoveryTest extends Stryker4sSuite with LogMatchers {
1919

2020
val group = groups.loneElement
2121
assertEquals(group.frameworkClass, "munit.Framework")
22-
assert(
23-
group.taskDefs.exists(_.fullyQualifiedName == "stryker4s.mill.runner.MillTestDiscoveryTest"),
24-
s"This test suite should be discovered, but got: ${group.taskDefs.map(_.fullyQualifiedName)}"
25-
)
22+
val taskDef = group.taskDefs
23+
.find(_.fullyQualifiedName == "stryker4s.mill.runner.MillTestDiscoveryTest")
24+
.getOrElse(
25+
fail(s"This test suite should be discovered, but got: ${group.taskDefs.map(_.fullyQualifiedName)}")
26+
)
27+
// Discovered tests are never explicitly specified on the command line
28+
assertEquals(taskDef.explicitlySpecified, false)
29+
assertNotLoggedWarn("No tests found for test framework")
2630
}
2731

2832
test("warns when no tests are found for the framework") {
2933
val groups = MillTestDiscovery.discover("munit.Framework", Seq.empty, fullClasspath)
3034

3135
assertEquals(groups.loneElement.taskDefs, Seq.empty)
32-
assertLoggedWarn("No tests found for test framework munit.Framework")
36+
assertLoggedWarn(
37+
"No tests found for test framework munit.Framework. " +
38+
"Will likely result in no tests being run and a NoCoverage result for all mutants."
39+
)
3340
}
3441
}

0 commit comments

Comments
 (0)