From 3bc429487d17081f6c7a4166a171922be0b1c3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ho=C3=A0ng?= <93896207+HollandDM@users.noreply.github.com> Date: Mon, 10 Mar 2025 13:12:36 +0700 Subject: [PATCH] Use process pool for test runner (0.12.x) (#4679) ported for 0.12.x from #4614 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .gitignore | 4 +- docs/modules/ROOT/pages/javalib/testing.adoc | 7 + .../modules/ROOT/pages/kotlinlib/testing.adoc | 7 + docs/modules/ROOT/pages/scalalib/testing.adoc | 8 + .../testing/5-test-stealing/build.mill | 15 + .../5-test-stealing/foo/src/foo/Foo.java | 11 + .../foo/test/src/foo/RandomTestsA.java | 11 + .../foo/test/src/foo/RandomTestsB.java | 10 + .../foo/test/src/foo/RandomTestsC.java | 10 + .../foo/test/src/foo/RandomTestsD.java | 10 + .../foo/test/src/foo/RandomTestsE.java | 10 + .../foo/test/src/foo/RandomTestsF.java | 10 + .../foo/test/src/foo/RandomTestsG.java | 10 + .../foo/test/src/foo/RandomTestsH.java | 10 + .../foo/test/src/foo/RandomTestsI.java | 10 + .../foo/test/src/foo/RandomTestsJ.java | 10 + .../foo/test/src/foo/RandomTestsUtils.java | 11 + .../testing/6-test-group-stealing/build.mill | 19 + .../foo/src/foo/Foo.java | 11 + .../foo/test/src/foo/GroupX1.java | 10 + .../foo/test/src/foo/GroupX10.java | 10 + .../foo/test/src/foo/GroupX2.java | 10 + .../foo/test/src/foo/GroupX3.java | 10 + .../foo/test/src/foo/GroupX4.java | 10 + .../foo/test/src/foo/GroupX5.java | 10 + .../foo/test/src/foo/GroupX6.java | 10 + .../foo/test/src/foo/GroupX7.java | 10 + .../foo/test/src/foo/GroupX8.java | 10 + .../foo/test/src/foo/GroupX9.java | 10 + .../foo/test/src/foo/GroupY1.java | 10 + .../foo/test/src/foo/GroupY10.java | 10 + .../foo/test/src/foo/GroupY2.java | 10 + .../foo/test/src/foo/GroupY3.java | 10 + .../foo/test/src/foo/GroupY4.java | 10 + .../foo/test/src/foo/GroupY5.java | 10 + .../foo/test/src/foo/GroupY6.java | 10 + .../foo/test/src/foo/GroupY7.java | 10 + .../foo/test/src/foo/GroupY8.java | 10 + .../foo/test/src/foo/GroupY9.java | 10 + .../foo/test/src/foo/RandomTestsUtils.java | 11 + .../testing/5-test-stealing/build.mill | 22 + .../5-test-stealing/foo/src/foo/Foo.kt | 11 + .../foo/test/src/foo/RandomTestsA.kt | 11 + .../foo/test/src/foo/RandomTestsB.kt | 11 + .../foo/test/src/foo/RandomTestsC.kt | 11 + .../foo/test/src/foo/RandomTestsD.kt | 11 + .../foo/test/src/foo/RandomTestsE.kt | 11 + .../foo/test/src/foo/RandomTestsF.kt | 11 + .../foo/test/src/foo/RandomTestsG.kt | 11 + .../foo/test/src/foo/RandomTestsH.kt | 11 + .../foo/test/src/foo/RandomTestsI.kt | 11 + .../foo/test/src/foo/RandomTestsJ.kt | 11 + .../foo/test/src/foo/RandomTestsUtils.kt | 12 + .../testing/6-test-group-stealing/build.mill | 26 ++ .../6-test-group-stealing/foo/src/foo/Foo.kt | 11 + .../foo/test/src/foo/GroupX1.kt | 11 + .../foo/test/src/foo/GroupX10.kt | 11 + .../foo/test/src/foo/GroupX2.kt | 11 + .../foo/test/src/foo/GroupX3.kt | 11 + .../foo/test/src/foo/GroupX4.kt | 11 + .../foo/test/src/foo/GroupX5.kt | 11 + .../foo/test/src/foo/GroupX6.kt | 11 + .../foo/test/src/foo/GroupX7.kt | 11 + .../foo/test/src/foo/GroupX8.kt | 11 + .../foo/test/src/foo/GroupX9.kt | 11 + .../foo/test/src/foo/GroupY1.kt | 11 + .../foo/test/src/foo/GroupY10.kt | 11 + .../foo/test/src/foo/GroupY2.kt | 11 + .../foo/test/src/foo/GroupY3.kt | 11 + .../foo/test/src/foo/GroupY4.kt | 11 + .../foo/test/src/foo/GroupY5.kt | 11 + .../foo/test/src/foo/GroupY6.kt | 11 + .../foo/test/src/foo/GroupY7.kt | 11 + .../foo/test/src/foo/GroupY8.kt | 11 + .../foo/test/src/foo/GroupY9.kt | 11 + .../foo/test/src/foo/RandomTestsUtils.kt | 12 + .../testing/5-test-stealing/build.mill | 41 ++ .../testing/5-test-stealing/foo/src/Foo.scala | 7 + .../foo/test/src/RandomTestsA.scala | 7 + .../foo/test/src/RandomTestsB.scala | 7 + .../foo/test/src/RandomTestsC.scala | 7 + .../foo/test/src/RandomTestsD.scala | 7 + .../foo/test/src/RandomTestsE.scala | 7 + .../foo/test/src/RandomTestsF.scala | 7 + .../foo/test/src/RandomTestsG.scala | 7 + .../foo/test/src/RandomTestsH.scala | 7 + .../foo/test/src/RandomTestsI.scala | 7 + .../foo/test/src/RandomTestsJ.scala | 7 + .../foo/test/src/RandomTestsUtils.scala | 16 + .../testing/6-test-group-stealing/build.mill | 51 +++ .../6-test-group-stealing/foo/src/Foo.scala | 7 + .../foo/test/src/GroupX1.scala | 7 + .../foo/test/src/GroupX10.scala | 7 + .../foo/test/src/GroupX2.scala | 7 + .../foo/test/src/GroupX3.scala | 7 + .../foo/test/src/GroupX4.scala | 7 + .../foo/test/src/GroupX5.scala | 7 + .../foo/test/src/GroupX6.scala | 7 + .../foo/test/src/GroupX7.scala | 7 + .../foo/test/src/GroupX8.scala | 7 + .../foo/test/src/GroupX9.scala | 7 + .../foo/test/src/GroupY1.scala | 7 + .../foo/test/src/GroupY10.scala | 7 + .../foo/test/src/GroupY2.scala | 7 + .../foo/test/src/GroupY3.scala | 7 + .../foo/test/src/GroupY4.scala | 7 + .../foo/test/src/GroupY5.scala | 7 + .../foo/test/src/GroupY6.scala | 7 + .../foo/test/src/GroupY7.scala | 7 + .../foo/test/src/GroupY8.scala | 7 + .../foo/test/src/GroupY9.scala | 7 + .../foo/test/src/RandomTestsUtils.scala | 14 + main/api/src/mill/api/Ctx.scala | 6 +- .../src/mill/eval/ExecutionContexts.scala | 9 +- scalalib/src/mill/scalalib/TestModule.scala | 25 +- .../src/mill/scalalib/TestModuleUtil.scala | 381 ++++++++++++++---- .../scalalib/TestRunnerScalatestTests.scala | 9 +- .../mill/scalalib/TestRunnerTestUtils.scala | 13 +- .../mill/scalalib/TestRunnerUtestTests.scala | 9 +- testrunner/src/mill/testrunner/Model.scala | 7 +- .../src/mill/testrunner/TestRunnerMain0.scala | 32 +- .../src/mill/testrunner/TestRunnerUtils.scala | 193 +++++++-- 122 files changed, 1705 insertions(+), 154 deletions(-) create mode 100644 example/javalib/testing/5-test-stealing/build.mill create mode 100644 example/javalib/testing/5-test-stealing/foo/src/foo/Foo.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.java create mode 100644 example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.java create mode 100644 example/javalib/testing/6-test-group-stealing/build.mill create mode 100644 example/javalib/testing/6-test-group-stealing/foo/src/foo/Foo.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.java create mode 100644 example/javalib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.java create mode 100644 example/kotlinlib/testing/5-test-stealing/build.mill create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/src/foo/Foo.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.kt create mode 100644 example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/build.mill create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/src/foo/Foo.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.kt create mode 100644 example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.kt create mode 100644 example/scalalib/testing/5-test-stealing/build.mill create mode 100644 example/scalalib/testing/5-test-stealing/foo/src/Foo.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsA.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsB.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsC.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsD.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsE.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsF.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsG.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsH.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsI.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsJ.scala create mode 100644 example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsUtils.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/build.mill create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/src/Foo.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX1.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX10.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX2.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX3.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX4.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX5.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX6.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX7.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX8.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX9.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY1.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY10.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY2.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY3.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY4.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY5.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY6.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY7.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY8.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY9.scala create mode 100644 example/scalalib/testing/6-test-group-stealing/foo/test/src/RandomTestsUtils.scala diff --git a/.gitignore b/.gitignore index fdcdd3f8ab7..37eb5b7d54e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ dist/ build/ *.bak mill-assembly.jar -mill-native \ No newline at end of file +mill-native +*.mill.orig +*.mill.rej \ No newline at end of file diff --git a/docs/modules/ROOT/pages/javalib/testing.adoc b/docs/modules/ROOT/pages/javalib/testing.adoc index ff8fde99d47..3a57827d87a 100644 --- a/docs/modules/ROOT/pages/javalib/testing.adoc +++ b/docs/modules/ROOT/pages/javalib/testing.adoc @@ -22,6 +22,13 @@ include::partial$example/javalib/testing/3-integration-suite.adoc[] include::partial$example/javalib/testing/4-test-grouping.adoc[] +== Test Work Stealing Scheduler + +include::partial$example/javalib/testing/5-test-stealing.adoc[] + +== Test Grouping & Test Work Stealing + +include::partial$example/javalib/testing/6-test-group-stealing.adoc[] == Github Actions Test Reports diff --git a/docs/modules/ROOT/pages/kotlinlib/testing.adoc b/docs/modules/ROOT/pages/kotlinlib/testing.adoc index 8c4c8500d32..b4477a14781 100644 --- a/docs/modules/ROOT/pages/kotlinlib/testing.adoc +++ b/docs/modules/ROOT/pages/kotlinlib/testing.adoc @@ -22,6 +22,13 @@ include::partial$example/kotlinlib/testing/3-integration-suite.adoc[] include::partial$example/kotlinlib/testing/4-test-grouping.adoc[] +== Test Work Stealing Scheduler + +include::partial$example/kotlinlib/testing/5-test-stealing.adoc[] + +== Test Grouping & Test Work Stealing + +include::partial$example/kotlinlib/testing/6-test-group-stealing.adoc[] == Github Actions Test Reports diff --git a/docs/modules/ROOT/pages/scalalib/testing.adoc b/docs/modules/ROOT/pages/scalalib/testing.adoc index a0447d39ce0..99f288a5903 100644 --- a/docs/modules/ROOT/pages/scalalib/testing.adoc +++ b/docs/modules/ROOT/pages/scalalib/testing.adoc @@ -22,6 +22,14 @@ include::partial$example/scalalib/testing/3-integration-suite.adoc[] include::partial$example/scalalib/testing/4-test-grouping.adoc[] +== Test Work Stealing Scheduler + +include::partial$example/scalalib/testing/5-test-stealing.adoc[] + +== Test Grouping & Test Work Stealing + +include::partial$example/scalalib/testing/6-test-group-stealing.adoc[] + == Github Actions Test Reports If you use Github Actions for CI, you can use https://github.com/mikepenz/action-junit-report in diff --git a/example/javalib/testing/5-test-stealing/build.mill b/example/javalib/testing/5-test-stealing/build.mill new file mode 100644 index 00000000000..f4262bcf319 --- /dev/null +++ b/example/javalib/testing/5-test-stealing/build.mill @@ -0,0 +1,15 @@ +//// SNIPPET:BUILD1 +package build +import mill._, javalib._ + +object foo extends JavaModule { + object test extends JavaTests { + def testFramework = "com.novocode.junit.JUnitFramework" + def ivyDeps = Agg( + ivy"com.novocode:junit-interface:0.11", + ivy"org.mockito:mockito-core:4.6.1" + ) + def testEnableWorkStealing = true + } +} +//// SNIPPET:END diff --git a/example/javalib/testing/5-test-stealing/foo/src/foo/Foo.java b/example/javalib/testing/5-test-stealing/foo/src/foo/Foo.java new file mode 100644 index 00000000000..0a99cb68d67 --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/src/foo/Foo.java @@ -0,0 +1,11 @@ +package foo; + +public class Foo { + public static void main(String[] args) { + System.out.println(greet("World")); + } + + public static String greet(String name) { + return "Hello " + name; + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.java new file mode 100644 index 00000000000..75337f0078d --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.java @@ -0,0 +1,11 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsA extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Storm", 38); + } + // Removed other tests to simplify +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.java new file mode 100644 index 00000000000..1d4ba5e1fdf --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsB extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Dakota", 18); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.java new file mode 100644 index 00000000000..02918a0a22a --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsC extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Jordan", 95); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.java new file mode 100644 index 00000000000..806dd50550b --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsD extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Kai", 14); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.java new file mode 100644 index 00000000000..e10e4568461 --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsE extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Sage", 28); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.java new file mode 100644 index 00000000000..340439deaa3 --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsF extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Winter", 12); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.java new file mode 100644 index 00000000000..37fa1bcc5c0 --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsG extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Finn", 45); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.java new file mode 100644 index 00000000000..9f568404d8e --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsH extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Haven", 22); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.java new file mode 100644 index 00000000000..44302550c27 --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsI extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Mars", 16); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.java new file mode 100644 index 00000000000..2fb787905bb --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class RandomTestsJ extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Storm", 38); + } +} diff --git a/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.java b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.java new file mode 100644 index 00000000000..ff21a48b29b --- /dev/null +++ b/example/javalib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.java @@ -0,0 +1,11 @@ +package foo; + +import static org.junit.Assert.assertEquals; + +public class RandomTestsUtils { + protected void testGreeting(String name, int sleepTime) throws Exception { + String greeted = Foo.greet(name); + Thread.sleep(sleepTime); + assertEquals("Hello " + name, greeted); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/build.mill b/example/javalib/testing/6-test-group-stealing/build.mill new file mode 100644 index 00000000000..2a4a3fd4503 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/build.mill @@ -0,0 +1,19 @@ +//// SNIPPET:BUILD1 +package build +import mill._, javalib._ + +object foo extends JavaModule { + object test extends JavaTests { + def testFramework = "com.novocode.junit.JUnitFramework" + def ivyDeps = Agg( + ivy"com.novocode:junit-interface:0.11", + ivy"org.mockito:mockito-core:4.6.1" + ) + def testForkGrouping = + discoveredTestClasses().groupMapReduce(_.contains("GroupX"))(Seq(_))(_ ++ _).toSeq.sortBy( + data => !data._1 + ).map(_._2) + def testEnableWorkStealing = true + } +} +//// SNIPPET:END diff --git a/example/javalib/testing/6-test-group-stealing/foo/src/foo/Foo.java b/example/javalib/testing/6-test-group-stealing/foo/src/foo/Foo.java new file mode 100644 index 00000000000..0a99cb68d67 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/src/foo/Foo.java @@ -0,0 +1,11 @@ +package foo; + +public class Foo { + public static void main(String[] args) { + System.out.println(greet("World")); + } + + public static String greet(String name) { + return "Hello " + name; + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.java new file mode 100644 index 00000000000..22aa90886aa --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX1 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Aether", 55); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.java new file mode 100644 index 00000000000..8c53423530e --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX10 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Echo", 52); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.java new file mode 100644 index 00000000000..c508fc8befb --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX2 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Chronos", 35); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.java new file mode 100644 index 00000000000..39e42f69a39 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX3 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Fortuna", 25); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.java new file mode 100644 index 00000000000..5d38ea5d644 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX4 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Janus", 21); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.java new file mode 100644 index 00000000000..042a7567ee8 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX5 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Orion", 95); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.java new file mode 100644 index 00000000000..0aeeaebc9ea --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX6 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Perseus", 34); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.java new file mode 100644 index 00000000000..01372630a40 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX7 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Selene", 52); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.java new file mode 100644 index 00000000000..664f2a618f1 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX8 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Uranus", 25); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.java new file mode 100644 index 00000000000..fcb065b5158 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupX9 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Ymir", 17); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.java new file mode 100644 index 00000000000..99038cf4088 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY1 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Hades", 15); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.java new file mode 100644 index 00000000000..60c5bca3289 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY10 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Rama", 25); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.java new file mode 100644 index 00000000000..eb77713cced --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY2 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Odin", 34); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.java new file mode 100644 index 00000000000..8b3dc5d8a4c --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY3 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Ra", 21); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.java new file mode 100644 index 00000000000..ae82b836aca --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY4 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Wotan", 95); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.java new file mode 100644 index 00000000000..2cb091e8d6f --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY5 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Xiuhtecuhtli", 26); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.java new file mode 100644 index 00000000000..2788e0bc99c --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY6 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Baldur", 17); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.java new file mode 100644 index 00000000000..f8442288ca1 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY7 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Horus", 34); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.java new file mode 100644 index 00000000000..e581738bce7 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY8 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Khonsu", 53); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.java new file mode 100644 index 00000000000..83f5742bf79 --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.java @@ -0,0 +1,10 @@ +package foo; + +import org.junit.Test; + +public class GroupY9 extends RandomTestsUtils { + @Test + public void test1() throws Exception { + testGreeting("Minerva", 21); + } +} diff --git a/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.java b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.java new file mode 100644 index 00000000000..ff21a48b29b --- /dev/null +++ b/example/javalib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.java @@ -0,0 +1,11 @@ +package foo; + +import static org.junit.Assert.assertEquals; + +public class RandomTestsUtils { + protected void testGreeting(String name, int sleepTime) throws Exception { + String greeted = Foo.greet(name); + Thread.sleep(sleepTime); + assertEquals("Hello " + name, greeted); + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/build.mill b/example/kotlinlib/testing/5-test-stealing/build.mill new file mode 100644 index 00000000000..84808520c19 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/build.mill @@ -0,0 +1,22 @@ +//// SNIPPET:BUILD1 +package build +import mill._, kotlinlib._ + +object foo extends KotlinModule { + + def mainClass = Some("foo.FooKt") + + def kotlinVersion = "1.9.24" + + object test extends KotlinTests { + def testFramework = "com.github.sbt.junit.jupiter.api.JupiterFramework" + def ivyDeps = Agg( + ivy"com.github.sbt.junit:jupiter-interface:0.11.4", + ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1", + ivy"org.mockito.kotlin:mockito-kotlin:5.4.0" + ) + + def testEnableWorkStealing = true + } +} +//// SNIPPET:END diff --git a/example/kotlinlib/testing/5-test-stealing/foo/src/foo/Foo.kt b/example/kotlinlib/testing/5-test-stealing/foo/src/foo/Foo.kt new file mode 100644 index 00000000000..5d549fb79eb --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/src/foo/Foo.kt @@ -0,0 +1,11 @@ +package foo + +object Foo { + @JvmStatic + fun main(args: Array) { + println(greet("World")) + } + + @JvmStatic + fun greet(name: String): String = "Hello $name" +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.kt new file mode 100644 index 00000000000..166d5cf75aa --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsA.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsA : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Storm", 38) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.kt new file mode 100644 index 00000000000..eb6a1597506 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsB.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsB : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Dakota", 18) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.kt new file mode 100644 index 00000000000..8c986e46824 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsC.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsC : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Jordan", 95) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.kt new file mode 100644 index 00000000000..9304dda44cc --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsD.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsD : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Kai", 14) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.kt new file mode 100644 index 00000000000..55ad80de805 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsE.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsE : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Sage", 28) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.kt new file mode 100644 index 00000000000..496b0006b8c --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsF.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsF : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Winter", 12) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.kt new file mode 100644 index 00000000000..8638a5ef1c3 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsG.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsG : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Finn", 45) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.kt new file mode 100644 index 00000000000..05d1913fc74 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsH.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsH : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Haven", 22) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.kt new file mode 100644 index 00000000000..56fde3cac20 --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsI.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsI : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Mars", 16) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.kt new file mode 100644 index 00000000000..948cfdde5ca --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsJ.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class RandomTestsJ : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Storm", 38) + } +} diff --git a/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.kt b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.kt new file mode 100644 index 00000000000..42e6839e75d --- /dev/null +++ b/example/kotlinlib/testing/5-test-stealing/foo/test/src/foo/RandomTestsUtils.kt @@ -0,0 +1,12 @@ +package foo + +import org.junit.jupiter.api.Assertions.assertEquals + +open class RandomTestsUtils { + @Throws(Exception::class) + protected fun testGreeting(name: String, sleepTime: Int) { + val greeted = Foo.greet(name) + Thread.sleep(sleepTime.toLong()) + assertEquals("Hello $name", greeted) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/build.mill b/example/kotlinlib/testing/6-test-group-stealing/build.mill new file mode 100644 index 00000000000..5c0943c2b91 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/build.mill @@ -0,0 +1,26 @@ +//// SNIPPET:BUILD1 +package build +import mill._, kotlinlib._ + +object foo extends KotlinModule { + + def mainClass = Some("foo.FooKt") + + def kotlinVersion = "1.9.24" + + object test extends KotlinTests { + def testFramework = "com.github.sbt.junit.jupiter.api.JupiterFramework" + def ivyDeps = Agg( + ivy"com.github.sbt.junit:jupiter-interface:0.11.4", + ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1", + ivy"org.mockito.kotlin:mockito-kotlin:5.4.0" + ) + + def testForkGrouping = + discoveredTestClasses().groupMapReduce(_.contains("GroupX"))(Seq(_))(_ ++ _).toSeq.sortBy( + data => !data._1 + ).map(_._2) + def testEnableWorkStealing = true + } +} +//// SNIPPET:END diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/src/foo/Foo.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/src/foo/Foo.kt new file mode 100644 index 00000000000..5d549fb79eb --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/src/foo/Foo.kt @@ -0,0 +1,11 @@ +package foo + +object Foo { + @JvmStatic + fun main(args: Array) { + println(greet("World")) + } + + @JvmStatic + fun greet(name: String): String = "Hello $name" +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.kt new file mode 100644 index 00000000000..bff63dac124 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX1.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX1 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Aether", 55) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.kt new file mode 100644 index 00000000000..9d4d6f25608 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX10.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX10 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Echo", 52) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.kt new file mode 100644 index 00000000000..4f3e49d4738 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX2.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX2 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Chronos", 35) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.kt new file mode 100644 index 00000000000..1f71a0f25af --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX3.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX3 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Fortuna", 25) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.kt new file mode 100644 index 00000000000..d76259bca72 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX4.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX4 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Janus", 21) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.kt new file mode 100644 index 00000000000..284f48f90d1 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX5.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX5 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Orion", 95) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.kt new file mode 100644 index 00000000000..e6611ea0115 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX6.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX6 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Perseus", 34) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.kt new file mode 100644 index 00000000000..14eaf3b6e2d --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX7.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX7 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Selene", 52) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.kt new file mode 100644 index 00000000000..33fa55a3278 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX8.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX8 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Uranus", 25) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.kt new file mode 100644 index 00000000000..e6d50458025 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupX9.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupX9 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Ymir", 17) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.kt new file mode 100644 index 00000000000..a6260b4f85a --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY1.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY1 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Hades", 15) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.kt new file mode 100644 index 00000000000..f5a4e71cd63 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY10.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY10 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Rama", 25) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.kt new file mode 100644 index 00000000000..b344676c823 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY2.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY2 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Odin", 34) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.kt new file mode 100644 index 00000000000..d639af9a233 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY3.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY3 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Ra", 21) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.kt new file mode 100644 index 00000000000..9637ac7b185 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY4.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY4 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Wotan", 95) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.kt new file mode 100644 index 00000000000..4b23ca15fb0 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY5.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY5 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Xiuhtecuhtli", 26) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.kt new file mode 100644 index 00000000000..fd4b90fab8e --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY6.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY6 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Baldur", 17) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.kt new file mode 100644 index 00000000000..4da4051126c --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY7.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY7 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Horus", 34) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.kt new file mode 100644 index 00000000000..e6f376cad28 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY8.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY8 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Khonsu", 53) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.kt new file mode 100644 index 00000000000..fec324099c2 --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/GroupY9.kt @@ -0,0 +1,11 @@ +package foo + +import org.junit.jupiter.api.Test + +class GroupY9 : RandomTestsUtils() { + @Test + @Throws(Exception::class) + fun test1() { + testGreeting("Minerva", 21) + } +} diff --git a/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.kt b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.kt new file mode 100644 index 00000000000..42e6839e75d --- /dev/null +++ b/example/kotlinlib/testing/6-test-group-stealing/foo/test/src/foo/RandomTestsUtils.kt @@ -0,0 +1,12 @@ +package foo + +import org.junit.jupiter.api.Assertions.assertEquals + +open class RandomTestsUtils { + @Throws(Exception::class) + protected fun testGreeting(name: String, sleepTime: Int) { + val greeted = Foo.greet(name) + Thread.sleep(sleepTime.toLong()) + assertEquals("Hello $name", greeted) + } +} diff --git a/example/scalalib/testing/5-test-stealing/build.mill b/example/scalalib/testing/5-test-stealing/build.mill new file mode 100644 index 00000000000..755843f4e3b --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/build.mill @@ -0,0 +1,41 @@ +// Test stealing is an opt-in, powerful feature that enables parallel test execution. +// When enabled, Mill automatically distributes your test classes across multiple JVM subprocesses. +// This is especially useful when you have multiple test classes that can be run in parallel. +// Work stealing scheduler ensures throughput is maximized: no "slow" tests can block "fast" tests. +// This feature is expected to be enabled by default in future versions of Mill. +// +// Test stealing can be enabled by overriding def testEnableWorkStealing, as demonstrated below. + +//// SNIPPET:BUILD1 +package build +import mill._, scalalib._ + +object foo extends ScalaModule { + def scalaVersion = "2.13.8" + object test extends ScalaTests { + def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.5") + def testFramework = "utest.runner.Framework" + + def testEnableWorkStealing = true + } +} + +//// SNIPPET:END + +/** Usage + +> mill -j 3 foo.test + +> find out/foo/test/test.dest +... +out/foo/test/test.dest/worker-0.log +out/foo/test/test.dest/worker-0 +out/foo/test/test.dest/worker-1.log +out/foo/test/test.dest/worker-1 +out/foo/test/test.dest/worker-2.log +out/foo/test/test.dest/worker-2 +out/foo/test/test.dest/test-classes +out/foo/test/test.dest/test-report.xml +... + +*/ diff --git a/example/scalalib/testing/5-test-stealing/foo/src/Foo.scala b/example/scalalib/testing/5-test-stealing/foo/src/Foo.scala new file mode 100644 index 00000000000..386104354fa --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/src/Foo.scala @@ -0,0 +1,7 @@ +package foo +object Foo { + def main(args: Array[String]): Unit = { + println(greet("World")) + } + def greet(name: String): String = s"Hello $name" +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsA.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsA.scala new file mode 100644 index 00000000000..5bf186d79b7 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsA.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsA extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Storm", 38) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsB.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsB.scala new file mode 100644 index 00000000000..2dff6d6de78 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsB.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsB extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Dakota", 18) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsC.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsC.scala new file mode 100644 index 00000000000..6711d6a6656 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsC.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsC extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Jordan", 95) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsD.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsD.scala new file mode 100644 index 00000000000..38eab20f7da --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsD.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsD extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Kai", 14) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsE.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsE.scala new file mode 100644 index 00000000000..8daaa738344 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsE.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsE extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Sage", 28) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsF.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsF.scala new file mode 100644 index 00000000000..79032b7ba12 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsF.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsF extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Winter", 12) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsG.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsG.scala new file mode 100644 index 00000000000..449b7991ce5 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsG.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsG extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Finn", 45) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsH.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsH.scala new file mode 100644 index 00000000000..c2862cecc3c --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsH.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsH extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Haven", 22) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsI.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsI.scala new file mode 100644 index 00000000000..18901f12053 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsI.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsI extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Mars", 16) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsJ.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsJ.scala new file mode 100644 index 00000000000..debdad4859d --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsJ.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object RandomTestsJ extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Storm", 38) } + } +} diff --git a/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsUtils.scala b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsUtils.scala new file mode 100644 index 00000000000..87adfe5d609 --- /dev/null +++ b/example/scalalib/testing/5-test-stealing/foo/test/src/RandomTestsUtils.scala @@ -0,0 +1,16 @@ +package foo +import utest._ + +abstract class RandomTestsUtils extends TestSuite { + + // sleepTime try to mimics real-life test time, + // sleepTime can be chosen randomly, but test classes should be "slow" (~100ms) + // enough so that cluster decide to spawn all available test runner processeses + // fail to do this can lead to flakky test. + def testGreeting(name: String, sleepTime: Int) = { + val greeted = Foo.greet(name) + Thread.sleep(sleepTime) + assert(greeted == s"Hello $name") + } + +} diff --git a/example/scalalib/testing/6-test-group-stealing/build.mill b/example/scalalib/testing/6-test-group-stealing/build.mill new file mode 100644 index 00000000000..7b4c4136916 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/build.mill @@ -0,0 +1,51 @@ +// `testEnableWorkStealing` respects `testForkGrouping`, allowing you to use both features in a test module. + +//// SNIPPET:BUILD1 +package build +import mill._, scalalib._ + +object foo extends ScalaModule { + def scalaVersion = "2.13.8" + object test extends ScalaTests { + def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.5") + def testFramework = "utest.runner.Framework" + + // Group tests by GroupX and GroupY + def testForkGrouping = + discoveredTestClasses().groupMapReduce(_.contains("GroupX"))(Seq(_))(_ ++ _).toSeq.sortBy( + data => !data._1 + ).map(_._2) + def testEnableWorkStealing = true + } +} + +//// SNIPPET:END + +/** Usage + +> mill -j 4 foo.test + +> find out/foo/test/test.dest +... +out/foo/test/test.dest/group-0-foo.GroupX1/worker-0.log +out/foo/test/test.dest/group-0-foo.GroupX1/worker-0 +out/foo/test/test.dest/group-0-foo.GroupX1/worker-1.log +out/foo/test/test.dest/group-0-foo.GroupX1/worker-1 +out/foo/test/test.dest/group-0-foo.GroupX1/test-classes +out/foo/test/test.dest/group-1-foo.GroupY1/worker-0.log +out/foo/test/test.dest/group-1-foo.GroupY1/worker-0 +out/foo/test/test.dest/group-1-foo.GroupY1/worker-1.log +out/foo/test/test.dest/group-1-foo.GroupY1/worker-1 +out/foo/test/test.dest/group-1-foo.GroupY1/test-classes +out/foo/test/test.dest/test-report.xml +... + +*/ + +// This example sets `testForkGrouping` to group test classes into two categories: `GroupX` and `GroupY`. +// Additionally, `testEnableWorkStealing` is enabled. +// Mill ensures each subprocess exclusively steals and runs tests from either `GroupX` or `GroupY`, preventing them from mixing. +// Test classes from `GroupX` and `GroupY` will never share the same test runner. +// +// This is useful when you have incompatible tests that cannot run within the same JVM. +// Test Grouping combined with Test Stealing Scheduler maintains their isolation while maximizing performance through parallel test execution. diff --git a/example/scalalib/testing/6-test-group-stealing/foo/src/Foo.scala b/example/scalalib/testing/6-test-group-stealing/foo/src/Foo.scala new file mode 100644 index 00000000000..386104354fa --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/src/Foo.scala @@ -0,0 +1,7 @@ +package foo +object Foo { + def main(args: Array[String]): Unit = { + println(greet("World")) + } + def greet(name: String): String = s"Hello $name" +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX1.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX1.scala new file mode 100644 index 00000000000..1b483ba1a38 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX1.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX1 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Aether", 55) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX10.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX10.scala new file mode 100644 index 00000000000..7fef760a674 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX10.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX10 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Echo", 52) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX2.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX2.scala new file mode 100644 index 00000000000..448943c5ea4 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX2.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX2 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Chronos", 35) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX3.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX3.scala new file mode 100644 index 00000000000..a6b2268a6ab --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX3.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX3 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Fortuna", 25) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX4.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX4.scala new file mode 100644 index 00000000000..b67e2f8365b --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX4.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX4 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Janus", 21) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX5.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX5.scala new file mode 100644 index 00000000000..44d42c80d25 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX5.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX5 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Orion", 95) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX6.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX6.scala new file mode 100644 index 00000000000..ec92a4c27e3 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX6.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX6 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Perseus", 34) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX7.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX7.scala new file mode 100644 index 00000000000..68983efe882 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX7.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX7 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Selene", 52) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX8.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX8.scala new file mode 100644 index 00000000000..85d4a1721a6 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX8.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX8 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Uranus", 25) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX9.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX9.scala new file mode 100644 index 00000000000..84518f008c8 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupX9.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupX9 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Ymir", 17) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY1.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY1.scala new file mode 100644 index 00000000000..448f840aad3 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY1.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY1 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Hades", 15) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY10.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY10.scala new file mode 100644 index 00000000000..ca7553275e1 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY10.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY10 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Rama", 25) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY2.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY2.scala new file mode 100644 index 00000000000..f1c66479a3c --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY2.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY2 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Odin", 34) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY3.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY3.scala new file mode 100644 index 00000000000..eed43b1aee7 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY3.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY3 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Ra", 21) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY4.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY4.scala new file mode 100644 index 00000000000..baa86a70269 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY4.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY4 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Wotan", 95) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY5.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY5.scala new file mode 100644 index 00000000000..6c05f52d644 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY5.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY5 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Xiuhtecuhtli", 26) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY6.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY6.scala new file mode 100644 index 00000000000..536292adc60 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY6.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY6 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Baldur", 17) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY7.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY7.scala new file mode 100644 index 00000000000..d2f761c3eda --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY7.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY7 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Horus", 34) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY8.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY8.scala new file mode 100644 index 00000000000..fdc676a27b0 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY8.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY8 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Khonsu", 53) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY9.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY9.scala new file mode 100644 index 00000000000..0c4ac5a14ae --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/GroupY9.scala @@ -0,0 +1,7 @@ +package foo +import utest._ +object GroupY9 extends RandomTestsUtils { + def tests = Tests { + test("test1") { testGreeting("Minerva", 21) } + } +} diff --git a/example/scalalib/testing/6-test-group-stealing/foo/test/src/RandomTestsUtils.scala b/example/scalalib/testing/6-test-group-stealing/foo/test/src/RandomTestsUtils.scala new file mode 100644 index 00000000000..49cb7a65a86 --- /dev/null +++ b/example/scalalib/testing/6-test-group-stealing/foo/test/src/RandomTestsUtils.scala @@ -0,0 +1,14 @@ +package foo +import utest._ + +abstract class RandomTestsUtils extends TestSuite { + // sleepTime try to mimics real-life test time, + // sleepTime can be chosen randomly, but test classes should be "slow" (~100ms) + // enough so that cluster decide to spawn all available test runner processeses + // fail to do this can lead to flakky test. + def testGreeting(name: String, sleepTime: Int): Unit = { + val greeted = Foo.greet(name) + Thread.sleep(sleepTime) + assert(greeted == s"Hello $name") + } +} diff --git a/main/api/src/mill/api/Ctx.scala b/main/api/src/mill/api/Ctx.scala index 8bba02268e1..f42915cb336 100644 --- a/main/api/src/mill/api/Ctx.scala +++ b/main/api/src/mill/api/Ctx.scala @@ -161,9 +161,13 @@ object Ctx { * terminal prompt to display what this future is currently computing. * @param t The body of the async future */ - def async[T](dest: os.Path, key: String, message: String)(t: => T)(implicit + def async[T](dest: os.Path, key: String, message: String, t: Logger => T)(implicit ctx: mill.api.Ctx ): Future[T] + + def async[T](dest: os.Path, key: String, message: String)(t: => T)(implicit + ctx: mill.api.Ctx + ): Future[T] = async(dest, key, message, _ => t)(ctx) } trait Impl extends Api with ExecutionContext with AutoCloseable { diff --git a/main/eval/src/mill/eval/ExecutionContexts.scala b/main/eval/src/mill/eval/ExecutionContexts.scala index 0384fe3477f..6fff3eec821 100644 --- a/main/eval/src/mill/eval/ExecutionContexts.scala +++ b/main/eval/src/mill/eval/ExecutionContexts.scala @@ -5,6 +5,7 @@ import os.Path import scala.concurrent.{Await, Future} import scala.concurrent.duration.Duration import java.util.concurrent.{ExecutorService, LinkedBlockingQueue, ThreadPoolExecutor, TimeUnit} +import mill.api.Logger private object ExecutionContexts { @@ -19,10 +20,10 @@ private object ExecutionContexts { def reportFailure(cause: Throwable): Unit = {} def close(): Unit = () // do nothing - def async[T](dest: Path, key: String, message: String)(t: => T)(implicit + def async[T](dest: Path, key: String, message: String, t: Logger => T)(implicit ctx: mill.api.Ctx ): Future[T] = - Future.successful(t) + Future.successful(t(ctx.log)) } /** @@ -81,7 +82,7 @@ private object ExecutionContexts { * folder [[dest]] and duplicates the logging streams to [[dest]].log while evaluating * [[t]], to avoid conflict with other tasks that may be running concurrently */ - def async[T](dest: Path, key: String, message: String)(t: => T)(implicit + def async[T](dest: Path, key: String, message: String, t: Logger => T)(implicit ctx: mill.api.Ctx ): Future[T] = { val logger = ctx.log.subLogger(dest / os.up / s"${dest.last}.log", key, message) @@ -99,7 +100,7 @@ private object ExecutionContexts { logger.withPrompt { os.dynamicPwdFunction.withValue(() => makeDest()) { mill.api.SystemStreams.withStreams(logger.systemStreams) { - t + t(logger) } } } diff --git a/scalalib/src/mill/scalalib/TestModule.scala b/scalalib/src/mill/scalalib/TestModule.scala index 476cbe165bd..4c5415f656e 100644 --- a/scalalib/src/mill/scalalib/TestModule.scala +++ b/scalalib/src/mill/scalalib/TestModule.scala @@ -96,15 +96,24 @@ trait TestModule } /** - * How the test classes in this module will be split into multiple JVM processes - * and run in parallel during testing. Defaults to all of them running in one process - * sequentially, but can be overridden to split them into separate groups that run - * in parallel. + * How the test classes in this module will be split. + * Test classes from different groups are ensured to never + * run on the same JVM process, and therefore can be run in parallel. + * When used in combination with [[testEnableWorkStealing]], + * every JVM test running process will guarantee to never steal tests + * from different test groups. */ def testForkGrouping: T[Seq[Seq[String]]] = Task { Seq(discoveredTestClasses()) } + /** + * Whether to use the test stealing scheduler to run tests in multiple JVM processes. + * When used in combination with [[testForkGrouping]], every JVM test running process + * will guarantee to never steal tests from different test groups. + */ + def testEnableWorkStealing: T[Boolean] = T(false) + /** * Discovers and runs the module's tests in a subprocess, reporting the * results to the console. @@ -157,7 +166,7 @@ trait TestModule colored = Task.log.colored, testCp = testClasspath().map(_.path), home = Task.home, - globSelectors = selectors + globSelectors = Left(selectors) ) val argsFile = Task.dest / "testargs" @@ -190,7 +199,7 @@ trait TestModule globSelectors: Task[Seq[String]] ): Task[(String, Seq[TestResult])] = Task.Anon { - TestModuleUtil.runTests( + val testModuleUtil = new TestModuleUtil( testUseArgsFile(), forkArgs(), globSelectors(), @@ -206,8 +215,10 @@ trait TestModule testSandboxWorkingDir(), forkWorkingDir(), testReportXml(), - zincWorker().javaHome().map(_.path) + zincWorker().javaHome().map(_.path), + testEnableWorkStealing() ) + testModuleUtil.runTests() } /** diff --git a/scalalib/src/mill/scalalib/TestModuleUtil.scala b/scalalib/src/mill/scalalib/TestModuleUtil.scala index e0527ff57f1..367508b6766 100644 --- a/scalalib/src/mill/scalalib/TestModuleUtil.scala +++ b/scalalib/src/mill/scalalib/TestModuleUtil.scala @@ -11,77 +11,43 @@ import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit import java.time.{Instant, LocalDateTime, ZoneId} import scala.xml.Elem - -private[scalalib] object TestModuleUtil { - def runTests( - useArgsFile: Boolean, - forkArgs: Seq[String], - selectors: Seq[String], - scalalibClasspath: Agg[PathRef], - resources: Seq[PathRef], - testFramework: String, - runClasspath: Seq[PathRef], - testClasspath: Seq[PathRef], - args: Seq[String], - testClassLists: Seq[Seq[String]], - testrunnerEntrypointClasspath: Agg[PathRef], - forkEnv: Map[String, String], - testSandboxWorkingDir: Boolean, - forkWorkingDir: os.Path, - testReportXml: Option[String], - javaHome: Option[os.Path] - )(implicit ctx: mill.api.Ctx) = { - - val (jvmArgs, props: Map[String, String]) = loadArgsAndProps(useArgsFile, forkArgs) - - val testRunnerClasspathArg = scalalibClasspath - .map(_.path.toNIO.toUri.toURL) - .mkString(",") - - val resourceEnv = Map( - EnvVars.MILL_TEST_RESOURCE_DIR -> resources.map(_.path).mkString(";"), - EnvVars.MILL_WORKSPACE_ROOT -> Task.workspace.toString - ) - - def runTestRunnerSubprocess(selectors2: Seq[String], base: os.Path) = { - val outputPath = base / "out.json" - val testArgs = TestArgs( - framework = testFramework, - classpath = runClasspath.map(_.path), - arguments = args, - sysProps = props, - outputPath = outputPath, - colored = Task.log.colored, - testCp = testClasspath.map(_.path), - home = Task.home, - globSelectors = selectors2 - ) - - val argsFile = base / "testargs" - val sandbox = base / "sandbox" - os.write(argsFile, upickle.default.write(testArgs), createFolders = true) - - os.makeDir.all(sandbox) - - Jvm.callProcess( - mainClass = "mill.testrunner.entrypoint.TestRunnerMain", - classPath = (runClasspath ++ testrunnerEntrypointClasspath).map(_.path), - jvmArgs = jvmArgs, - env = forkEnv ++ resourceEnv, - mainArgs = Seq(testRunnerClasspathArg, argsFile.toString), - cwd = if (testSandboxWorkingDir) sandbox else forkWorkingDir, - cpPassingJarPath = Option.when(useArgsFile)( - os.temp(prefix = "run-", suffix = ".jar", deleteOnExit = false) - ), - javaHome = javaHome, - stdin = os.Inherit, - stdout = os.Inherit - ) - - if (!os.exists(outputPath)) Left(s"Test reporting Failed: ${outputPath} does not exist") - else Right(upickle.default.read[(String, Seq[TestResult])](ujson.read(outputPath.toIO))) - } - +import scala.collection.mutable +import mill.api.Logger +import java.util.concurrent.Executors + +private final class TestModuleUtil( + useArgsFile: Boolean, + forkArgs: Seq[String], + selectors: Seq[String], + scalalibClasspath: Agg[PathRef], + resources: Seq[PathRef], + testFramework: String, + runClasspath: Seq[PathRef], + testClasspath: Seq[PathRef], + args: Seq[String], + testClassLists: Seq[Seq[String]], + testrunnerEntrypointClasspath: Agg[PathRef], + forkEnv: Map[String, String], + testSandboxWorkingDir: Boolean, + forkWorkingDir: os.Path, + testReportXml: Option[String], + javaHome: Option[os.Path], + testEnableWorkStealing: Boolean +)(implicit ctx: mill.api.Ctx) { + + private val (jvmArgs, props: Map[String, String]) = + TestModuleUtil.loadArgsAndProps(useArgsFile, forkArgs) + + private val testRunnerClasspathArg = scalalibClasspath + .map(_.path.toNIO.toUri.toURL) + .mkString(",") + + private val resourceEnv = Map( + EnvVars.MILL_TEST_RESOURCE_DIR -> resources.map(_.path).mkString(";"), + EnvVars.MILL_WORKSPACE_ROOT -> Task.workspace.toString + ) + + def runTests(): Result[(String, Seq[TestResult])] = { val globFilter = TestRunnerUtils.globFilter(selectors) def doesNotMatchError = Result.Failure( @@ -133,18 +99,89 @@ private[scalalib] object TestModuleUtil { } if (selectors.nonEmpty && filteredClassLists.isEmpty) throw doesNotMatchError - val subprocessResult: Either[String, (String, Seq[TestResult])] = filteredClassLists match { + val result = if (testEnableWorkStealing) { + runTestStealingScheduler(filteredClassLists) + } else { + runTestDefault(filteredClassLists) + } + + result match { + case Left(errMsg) => Result.Failure(errMsg) + case Right((doneMsg, results)) => + if (results.isEmpty && selectors.nonEmpty) throw doesNotMatchError + try TestModuleUtil.handleResults(doneMsg, results, Task.ctx(), testReportXml) + catch { + case e: Throwable => Result.Failure("Test reporting failed: " + e) + } + } + } + + private def callTestRunnerSubprocess( + baseFolder: os.Path, + // either: + // - Left(selectors: Seq[String]): - list of glob selectors to feed to the test runner directly. + // - Right((testClassesFolder: os.Path, stealFolder: os.Path)): - folder containing test classes for test runner to steal from, and the stealer's base folder. + selector: Either[Seq[String], (os.Path, os.Path)] + )(implicit ctx: mill.api.Ctx) = { + os.makeDir.all(baseFolder) + + val outputPath = baseFolder / "out.json" + val testArgs = TestArgs( + framework = testFramework, + classpath = runClasspath.map(_.path), + arguments = args, + sysProps = props, + outputPath = outputPath, + colored = Task.log.colored, + testCp = testClasspath.map(_.path), + home = Task.home, + globSelectors = selector + ) + + val argsFile = baseFolder / "testargs" + val sandbox = baseFolder / "sandbox" + os.write(argsFile, upickle.default.write(testArgs), createFolders = true) + + os.makeDir.all(sandbox) + + Jvm.callProcess( + mainClass = "mill.testrunner.entrypoint.TestRunnerMain", + classPath = (runClasspath ++ testrunnerEntrypointClasspath).map(_.path), + jvmArgs = jvmArgs, + env = forkEnv ++ resourceEnv, + mainArgs = Seq(testRunnerClasspathArg, argsFile.toString), + cwd = if (testSandboxWorkingDir) sandbox else forkWorkingDir, + cpPassingJarPath = Option.when(useArgsFile)( + os.temp(prefix = "run-", suffix = ".jar", deleteOnExit = false) + ), + javaHome = javaHome, + stdin = os.Inherit, + stdout = os.Inherit + ) + + if (!os.exists(outputPath)) Left(s"Test reporting Failed: ${outputPath} does not exist") + else Right(upickle.default.read[(String, Seq[TestResult])](ujson.read(outputPath.toIO))) + } + + private def runTestDefault( + filteredClassLists: Seq[Seq[String]] + )(implicit ctx: mill.api.Ctx) = { + + val subprocessResult = filteredClassLists match { // When no tests at all are discovered, run at least one test JVM // process to go through the test framework setup/teardown logic - case Nil => runTestRunnerSubprocess(Nil, Task.dest) - case Seq(singleTestClassList) => runTestRunnerSubprocess(singleTestClassList, Task.dest) + case Nil => callTestRunnerSubprocess(Task.dest, Left(Nil)) + case Seq(singleTestClassList) => + callTestRunnerSubprocess(Task.dest, Left(singleTestClassList)) case multipleTestClassLists => val maxLength = multipleTestClassLists.length.toString.length val futures = multipleTestClassLists.zipWithIndex.map { case (testClassList, i) => val groupPromptMessage = testClassList match { case Seq(single) => single case multiple => - collapseTestClassNames(multiple).mkString(", ") + s", ${multiple.length} suites" + TestModuleUtil.collapseTestClassNames( + multiple + ).mkString(", ") + s", ${multiple.length} suites" } val paddedIndex = mill.util.Util.leftPad(i.toString, maxLength, '0') @@ -155,7 +192,7 @@ private[scalalib] object TestModuleUtil { } Task.fork.async(Task.dest / folderName, paddedIndex, groupPromptMessage) { - (folderName, runTestRunnerSubprocess(testClassList, Task.dest / folderName)) + (folderName, callTestRunnerSubprocess(Task.dest / folderName, Left(testClassList))) } } @@ -170,18 +207,194 @@ private[scalalib] object TestModuleUtil { else Right((rights.map(_._1).mkString("\n"), rights.flatMap(_._2))) } - subprocessResult match { - case Left(errMsg) => Result.Failure(errMsg) - case Right((doneMsg, results)) => - if (results.isEmpty && selectors.nonEmpty) throw doesNotMatchError - try handleResults(doneMsg, results, Task.ctx(), testReportXml) - catch { - case e: Throwable => Result.Failure("Test reporting failed: " + e) + subprocessResult + } + + private def runTestStealingScheduler( + filteredClassLists: Seq[Seq[String]] + )(implicit ctx: mill.api.Ctx) = { + + val workerStatusMap = new java.util.concurrent.ConcurrentHashMap[os.Path, String => Unit]() + + def prepareTestClassesFolder(selectors2: Seq[String], base: os.Path): os.Path = { + // test-classes folder is used to store the test classes for the children test runners to steal from + val testClassesFolder = base / "test-classes" + os.makeDir.all(testClassesFolder) + selectors2.zipWithIndex.foreach { case (s, i) => + os.write.over(testClassesFolder / s, Array.empty[Byte]) + } + testClassesFolder + } + + def runTestRunnerSubprocess( + base: os.Path, + testClassesFolder: os.Path, + force: Boolean, + logger: Logger + ) = { + // Check if we really need to spawn a new runner + if (force || os.list(testClassesFolder).nonEmpty) { + val stealFolder = base / "steal" + os.makeDir.all(stealFolder) + // steal.log file will be appended by the runner with the stolen test class's name + // it can be used to check the order of test classes of the runner + val stealLog = stealFolder / os.up / s"${stealFolder.last}.log" + os.write.over(stealLog, Array.empty[Byte]) + workerStatusMap.put(stealLog, logger.ticker) + val result = callTestRunnerSubprocess(base, Right(testClassesFolder -> stealFolder)) + workerStatusMap.remove(stealLog) + Some(result) + } else { + None + } + } + + val groupFolderData = filteredClassLists match { + case Nil => Seq((Task.dest, prepareTestClassesFolder(Nil, Task.dest), 0)) + case Seq(singleTestClassList) => + Seq(( + Task.dest, + prepareTestClassesFolder(singleTestClassList, Task.dest), + singleTestClassList.length + )) + case multipleTestClassLists => + val maxLength = multipleTestClassLists.length.toString.length + multipleTestClassLists.zipWithIndex.map { case (testClassList, i) => + val paddedIndex = mill.util.Util.leftPad(i.toString, maxLength, '0') + val folderName = testClassList match { + case Seq(single) => single + case multiple => + s"group-$paddedIndex-${multiple.head}" + } + + ( + Task.dest / folderName, + prepareTestClassesFolder(testClassList, Task.dest / folderName), + testClassList.length + ) + } + } + + val jobs = Task.ctx() match { + case j: Ctx.Jobs => j.jobs + case _ => 1 + } + + val maxProcessLength = jobs.toString.length + val groupLength = groupFolderData.length + val maxGroupLength = groupLength.toString.length + + // We got "--jobs" threads, and "groupLength" test groups, so we will spawn at most jobs * groupLength runners here + // In most case, this is more than necessary, and runner creation is expensive, + // but we have a check for non-empty test-classes folder before really spawning a new runner, so in practice the overhead is low + val subprocessFutures = groupFolderData match { + case Seq(singleGroupFolderData) => + val (groupFolder, testClassesFolder, numTests) = singleGroupFolderData + val groupName = groupFolder.last + for (processIndex <- 0 until Math.max(Math.min(jobs, numTests), 1)) yield { + val paddedProcessIndex = + mill.util.Util.leftPad(processIndex.toString, maxProcessLength, '0') + val processFolder = groupFolder / s"worker-$paddedProcessIndex" + Task.fork.async( + processFolder, + paddedProcessIndex, + s"worker-$paddedProcessIndex", + logger => + // force run when processIndex == 0 (first subprocess), even if there are no tests to run + // to force the process to go through the test framework setup/teardown logic + groupName -> runTestRunnerSubprocess( + processFolder, + testClassesFolder, + force = processIndex == 0, + logger + ) + ) + } + case multipleGroupFolderData => + for { + ((groupFolder, testClassesFolder, numTests), groupIndex) <- groupFolderData.zipWithIndex + // Don't have re-calculate for every processes + groupName = groupFolder.last + paddedGroupIndex = mill.util.Util.leftPad(groupIndex.toString, maxGroupLength, '0') + processIndex <- 0 until Math.max(Math.min(jobs, numTests), 1) + } yield { + val paddedProcessIndex = + mill.util.Util.leftPad(processIndex.toString, maxProcessLength, '0') + val processFolder = groupFolder / s"worker-$paddedProcessIndex" + + Task.fork.async( + processFolder, + s"${paddedGroupIndex}-${paddedProcessIndex}", + s"worker-${paddedGroupIndex}-${paddedProcessIndex}", + logger => + // force run when processIndex == 0 (first subprocess), even if there are no tests to run + // to force the process to go through the test framework setup/teardown logic + groupName -> runTestRunnerSubprocess( + processFolder, + testClassesFolder, + force = processIndex == 0, + logger + ) + ) } } + + val executor = Executors.newScheduledThreadPool(1) + val outputs = + try { + // Periodically check the stealLog file of every runner, and tick the executing test name + executor.scheduleWithFixedDelay( + () => { + workerStatusMap.forEach { (stealLog, callback) => + { + try { + // the last one is always the latest + os.read.lines(stealLog).lastOption.foreach(callback) + } finally () + } + } + }, + 0, + 1, + java.util.concurrent.TimeUnit.SECONDS + ) + + Task.fork.awaitAll(subprocessFutures) + } finally { + executor.shutdown() + } + + val subprocessResult = { + val failMap = mutable.Map.empty[String, String] + val successMap = mutable.Map.empty[String, (String, Seq[TestResult])] + + outputs.foreach { + case (name, Some(Left(v))) => failMap.updateWith(name) { + case Some(old) => Some(old + " " + v) + case None => Some(v) + } + case (name, Some(Right((msg, results)))) => successMap.updateWith(name) { + case Some((oldMsg, oldResults)) => Some((oldMsg + " " + msg, oldResults ++ results)) + case None => Some((msg, results)) + } + case _ => () + } + + if (failMap.nonEmpty) { + Left(failMap.values.mkString("\n")) + } else { + Right((successMap.values.map(_._1).mkString("\n"), successMap.values.flatMap(_._2).toSeq)) + } + } + + subprocessResult } - private def loadArgsAndProps(useArgsFile: Boolean, forkArgs: Seq[String]) = { +} + +private[scalalib] object TestModuleUtil { + + private[scalalib] def loadArgsAndProps(useArgsFile: Boolean, forkArgs: Seq[String]) = { if (useArgsFile) { val (props, jvmArgs) = forkArgs.partition(_.startsWith("-D")) val sysProps = diff --git a/scalalib/test/src/mill/scalalib/TestRunnerScalatestTests.scala b/scalalib/test/src/mill/scalalib/TestRunnerScalatestTests.scala index 0b92209cfa2..7c439fa4674 100644 --- a/scalalib/test/src/mill/scalalib/TestRunnerScalatestTests.scala +++ b/scalalib/test/src/mill/scalalib/TestRunnerScalatestTests.scala @@ -33,7 +33,8 @@ object TestRunnerScalatestTests extends TestSuite { Map( // No test grouping is triggered because we only run one test class testrunner.scalatest -> Set("out.json", "sandbox", "test-report.xml", "testargs"), - testrunnerGrouping.scalatest -> Set("out.json", "sandbox", "test-report.xml", "testargs") + testrunnerGrouping.scalatest -> Set("out.json", "sandbox", "test-report.xml", "testargs"), + testrunnerWorkStealing.scalatest -> Set("worker-0", "test-classes", "test-report.xml") ) ) @@ -47,7 +48,8 @@ object TestRunnerScalatestTests extends TestSuite { "group-0-mill.scalalib.ScalaTestSpec", "mill.scalalib.ScalaTestSpec3", "test-report.xml" - ) + ), + testrunnerWorkStealing.scalatest -> Set("worker-0", "test-classes", "test-report.xml") ) ) test("include") - tester.testOnly( @@ -79,7 +81,8 @@ object TestRunnerScalatestTests extends TestSuite { "group-0-mill.scalalib.ScalaTestSpec", "mill.scalalib.ScalaTestSpec3", "test-report.xml" - ) + ), + testrunnerWorkStealing.scalatest -> Set("worker-0", "test-classes", "test-report.xml") ) ) test("includeAndExclude") - tester.testOnly0 { (eval, mod) => diff --git a/scalalib/test/src/mill/scalalib/TestRunnerTestUtils.scala b/scalalib/test/src/mill/scalalib/TestRunnerTestUtils.scala index 06a6eca9f19..884e95d3115 100644 --- a/scalalib/test/src/mill/scalalib/TestRunnerTestUtils.scala +++ b/scalalib/test/src/mill/scalalib/TestRunnerTestUtils.scala @@ -14,18 +14,27 @@ import scala.xml.{Elem, NodeSeq, XML} object TestRunnerTestUtils { object testrunner extends TestRunnerTestModule { def computeTestForkGrouping(x: Seq[String]) = Seq(x) + def enableWorkStealing = false } object testrunnerGrouping extends TestRunnerTestModule { def computeTestForkGrouping(x: Seq[String]) = x.sorted.grouped(2).toSeq + def enableWorkStealing = false + } + + object testrunnerWorkStealing extends TestRunnerTestModule { + def computeTestForkGrouping(x: Seq[String]) = Seq(x) + def enableWorkStealing = true } trait TestRunnerTestModule extends TestBaseModule with ScalaModule { def computeTestForkGrouping(x: Seq[String]): Seq[Seq[String]] + def enableWorkStealing: Boolean def scalaVersion = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???) object utest extends ScalaTests with TestModule.Utest { override def testForkGrouping = computeTestForkGrouping(discoveredTestClasses()) + override def testEnableWorkStealing = enableWorkStealing override def ivyDeps = Task { super.ivyDeps() ++ Agg( ivy"com.lihaoyi::utest:${sys.props.getOrElse("TEST_UTEST_VERSION", ???)}" @@ -35,6 +44,7 @@ object TestRunnerTestUtils { object scalatest extends ScalaTests with TestModule.ScalaTest { override def testForkGrouping = computeTestForkGrouping(discoveredTestClasses()) + override def testEnableWorkStealing = enableWorkStealing override def ivyDeps = Task { super.ivyDeps() ++ Agg( ivy"org.scalatest::scalatest:${sys.props.getOrElse("TEST_SCALATEST_VERSION", ???)}" @@ -62,6 +72,7 @@ object TestRunnerTestUtils { object ziotest extends ScalaTests with TestModule.ZioTest { override def testForkGrouping = computeTestForkGrouping(discoveredTestClasses()) + override def testEnableWorkStealing = enableWorkStealing override def ivyDeps = Task { super.ivyDeps() ++ Agg( ivy"dev.zio::zio-test:${sys.props.getOrElse("TEST_ZIOTEST_VERSION", ???)}", @@ -75,7 +86,7 @@ object TestRunnerTestUtils { class TestOnlyTester(m: TestRunnerTestModule => TestModule) { def testOnly0(f: (UnitTester, TestRunnerTestModule) => Unit) = { - for (mod <- Seq(testrunner, testrunnerGrouping)) { + for (mod <- Seq(testrunner, testrunnerGrouping, testrunnerWorkStealing)) { UnitTester(mod, resourcePath).scoped { eval => f(eval, mod) } } } diff --git a/scalalib/test/src/mill/scalalib/TestRunnerUtestTests.scala b/scalalib/test/src/mill/scalalib/TestRunnerUtestTests.scala index e43af5863c2..697f63b1a79 100644 --- a/scalalib/test/src/mill/scalalib/TestRunnerUtestTests.scala +++ b/scalalib/test/src/mill/scalalib/TestRunnerUtestTests.scala @@ -38,7 +38,8 @@ object TestRunnerUtestTests extends TestSuite { Map( testrunner.utest -> Set("out.json", "sandbox", "test-report.xml", "testargs"), // When there is only one test group with test classes, we do not put it in a subfolder - testrunnerGrouping.utest -> Set("out.json", "sandbox", "test-report.xml", "testargs") + testrunnerGrouping.utest -> Set("out.json", "sandbox", "test-report.xml", "testargs"), + testrunnerWorkStealing.utest -> Set("worker-0", "test-classes", "test-report.xml") ) ) test("multi") - tester.testOnly( @@ -52,7 +53,8 @@ object TestRunnerUtestTests extends TestSuite { "mill.scalalib.BarTests", "mill.scalalib.FoobarTests", "test-report.xml" - ) + ), + testrunnerWorkStealing.utest -> Set("worker-0", "test-classes", "test-report.xml") ) ) test("all") - tester.testOnly( @@ -67,7 +69,8 @@ object TestRunnerUtestTests extends TestSuite { "group-0-mill.scalalib.BarTests", "mill.scalalib.FoobarTests", "test-report.xml" - ) + ), + testrunnerWorkStealing.utest -> Set("worker-0", "test-classes", "test-report.xml") ) ) test("noMatch") - tester.testOnly0 { (eval, mod) => diff --git a/testrunner/src/mill/testrunner/Model.scala b/testrunner/src/mill/testrunner/Model.scala index 20fb5472b8d..7ae41bdac97 100644 --- a/testrunner/src/mill/testrunner/Model.scala +++ b/testrunner/src/mill/testrunner/Model.scala @@ -12,7 +12,12 @@ import mill.api.internal colored: Boolean, testCp: Seq[os.Path], home: os.Path, - globSelectors: Seq[String] + // globSelectors indicates the strategy for testrunner to find and run test classes + // can be either: + // - Left(selectors: Seq[String]): - list of glob selectors, testrunner is given a list of glob selectors to run directly + // - Right((selectorFolder: os.Path, baseFolder: os.Path)): - a pair of paths, testrunner will try to steal test glob from selectorFolder + // and move it actomatically in to baseFolder and run it from there. + globSelectors: Either[Seq[String], (os.Path, os.Path)] ) @internal object TestArgs { diff --git a/testrunner/src/mill/testrunner/TestRunnerMain0.scala b/testrunner/src/mill/testrunner/TestRunnerMain0.scala index a0656cefb6d..ad03f79e689 100644 --- a/testrunner/src/mill/testrunner/TestRunnerMain0.scala +++ b/testrunner/src/mill/testrunner/TestRunnerMain0.scala @@ -26,16 +26,28 @@ import mill.util.PrintLogger ctx.log.debug(s"Setting ${testArgs.sysProps.size} system properties") testArgs.sysProps.foreach { case (k, v) => System.setProperty(k, v) } - val filter = TestRunnerUtils.globFilter(testArgs.globSelectors) - - val result = TestRunnerUtils.runTestFramework0( - frameworkInstances = Framework.framework(testArgs.framework), - testClassfilePath = Agg.from(testArgs.testCp), - args = testArgs.arguments, - classFilter = cls => filter(cls.getName), - cl = classLoader, - testReporter = DummyTestReporter - )(ctx) + val result = testArgs.globSelectors match { + case Left(selectors) => + val filter = TestRunnerUtils.globFilter(selectors) + TestRunnerUtils.runTestFramework0( + frameworkInstances = Framework.framework(testArgs.framework), + testClassfilePath = Agg.from(testArgs.testCp), + args = testArgs.arguments, + classFilter = cls => filter(cls.getName), + cl = classLoader, + testReporter = DummyTestReporter + )(ctx) + case Right((testClassesFolder, stealFolder)) => + TestRunnerUtils.stealTestFramework0( + frameworkInstances = Framework.framework(testArgs.framework), + testClassfilePath = Agg.from(testArgs.testCp), + args = testArgs.arguments, + testClassesFolder = testClassesFolder, + stealFolder = stealFolder, + cl = classLoader, + testReporter = DummyTestReporter + )(ctx) + } // Clear interrupted state in case some badly-behaved test suite // dirtied the thread-interrupted flag and forgot to clean up. Otherwise, diff --git a/testrunner/src/mill/testrunner/TestRunnerUtils.scala b/testrunner/src/mill/testrunner/TestRunnerUtils.scala index 563b1e480c3..1d1049552ae 100644 --- a/testrunner/src/mill/testrunner/TestRunnerUtils.scala +++ b/testrunner/src/mill/testrunner/TestRunnerUtils.scala @@ -15,6 +15,8 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala @internal object TestRunnerUtils { + private type ClassWithFingerprint = (Class[?], Fingerprint) + def listClassFiles(base: os.Path): geny.Generator[String] = { if (os.isDir(base)) { os.walk.stream(base).filter(_.ext == "class").map(_.relativeTo(base).toString) @@ -36,7 +38,7 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala cl: ClassLoader, framework: Framework, classpath: Loose.Agg[os.Path] - ): Loose.Agg[(Class[_], Fingerprint)] = { + ): Loose.Agg[ClassWithFingerprint] = { val fingerprints = framework.fingerprints() @@ -45,7 +47,7 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala // the tests to run Instead just don't run anything .filter(os.exists(_)) .flatMap { base => - Loose.Agg.from[(Class[_], Fingerprint)]( + Loose.Agg.from[ClassWithFingerprint]( listClassFiles(base).map { path => val cls = cl.loadClass(path.stripSuffix(".class").replace('/', '.')) val publicConstructorCount = @@ -85,7 +87,7 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala cls: Class[_], fingerprints: Array[Fingerprint], isModule: Boolean - ): Option[(Class[_], Fingerprint)] = { + ): Option[ClassWithFingerprint] = { fingerprints.find { case f: SubclassFingerprint => f.isModule == isModule && @@ -129,45 +131,38 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala (runner, tasks) } - def runTasks(tasks: Seq[Task], testReporter: TestReporter, runner: Runner)(implicit - ctx: Ctx.Log with Ctx.Home - ): (String, Iterator[TestResult]) = { - val events = new ConcurrentLinkedQueue[Event]() - val doneMessage = { - - val taskQueue = tasks.to(mutable.Queue) - while (taskQueue.nonEmpty) { - val next = taskQueue.dequeue().execute( - new EventHandler { - def handle(event: Event) = { - testReporter.logStart(event) - events.add(event) - testReporter.logFinish(event) - } - }, - Array(new Logger { - def debug(msg: String) = ctx.log.outputStream.println(msg) - def error(msg: String) = ctx.log.outputStream.println(msg) - def ansiCodesSupported() = true - def warn(msg: String) = ctx.log.outputStream.println(msg) - def trace(t: Throwable) = t.printStackTrace(ctx.log.outputStream) - def info(msg: String) = ctx.log.outputStream.println(msg) - }) - ) - - taskQueue.enqueueAll(next) - } - runner.done() - } + private def executeTasks( + tasks: Seq[Task], + testReporter: TestReporter, + runner: Runner, + events: ConcurrentLinkedQueue[Event] + )(implicit ctx: Ctx.Log with Ctx.Home): Unit = { + val taskQueue = tasks.to(mutable.Queue) + while (taskQueue.nonEmpty) { + val next = taskQueue.dequeue().execute( + new EventHandler { + def handle(event: Event) = { + testReporter.logStart(event) + events.add(event) + testReporter.logFinish(event) + } + }, + Array(new Logger { + def debug(msg: String) = ctx.log.outputStream.println(msg) + def error(msg: String) = ctx.log.outputStream.println(msg) + def ansiCodesSupported() = true + def warn(msg: String) = ctx.log.outputStream.println(msg) + def trace(t: Throwable) = t.printStackTrace(ctx.log.outputStream) + def info(msg: String) = ctx.log.outputStream.println(msg) + }) + ) - if (doneMessage != null && doneMessage.nonEmpty) { - if (doneMessage.endsWith("\n")) - ctx.log.outputStream.print(doneMessage) - else - ctx.log.outputStream.println(doneMessage) + taskQueue.enqueueAll(next) } + } - val results = for (e <- events.iterator().asScala) yield { + def parseRunTaskResults(events: Iterator[Event]): Iterator[TestResult] = { + for (e <- events) yield { val ex = if (e.throwable().isDefined) Some(e.throwable().get) else None mill.testrunner.TestResult( @@ -186,10 +181,35 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala ex.map(_.getStackTrace.toIndexedSeq) ) } + } + + private def handleRunnerDone( + runner: Runner, + events: ConcurrentLinkedQueue[Event] + ): (String, Iterator[TestResult]) = { + val doneMessage = runner.done() + if (doneMessage != null && doneMessage.nonEmpty) { + if (doneMessage.endsWith("\n")) + println(doneMessage.stripSuffix("\n")) + else + println(doneMessage) + } + + val results = parseRunTaskResults(events.iterator().asScala) (doneMessage, results) } + def runTasks(tasks: Seq[Task], testReporter: TestReporter, runner: Runner)(implicit + ctx: Ctx.Log with Ctx.Home + ): (String, Iterator[TestResult]) = { + // Capture this value outside of the task event handler so it + // isn't affected by a test framework's stream redirects + val events = new ConcurrentLinkedQueue[Event]() + executeTasks(tasks, testReporter, runner, events) + handleRunnerDone(runner, events) + } + def runTestFramework0( frameworkInstances: ClassLoader => Framework, testClassfilePath: Loose.Agg[Path], @@ -208,6 +228,101 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala (doneMessage, results.toSeq) } + private def stealTaskFromTestClassesFolder( + globSelectorCache: Map[String, ClassWithFingerprint], + runner: Runner, + stealFolder: os.Path, + testClassesFolder: os.Path + ): Array[Task] = { + // append only log, used to communicate with parent about what test is being stolen + // so that the parent can log the stolen test's name to its logger + val stealLog = stealFolder / os.up / s"${stealFolder.last}.log" + val files = os.list.stream(testClassesFolder).toBuffer + while (files.nonEmpty) { + val file = files.remove(0) + val stolenFile = stealFolder / file.last + val stole = + try { + // we can check for existence of stolenFile first, but it'll require another os call. + // it just better to let this call failed in that case. + os.move( + file, + stolenFile, + atomicMove = true + ) + true + } catch { + case e: Exception => false + } + if (stole) { + val selector = stolenFile.last + os.write.append(stealLog, s"$selector\n") + val taskDefs = globSelectorCache.get(stolenFile.last) match { + case Some((cls, fingerprint)) => + Array(new TaskDef( + cls.getName.stripSuffix("$"), + fingerprint, + false, + Array(new SuiteSelector) + )) + case None => Array.empty[TaskDef] + } + val tasks = runner.tasks(taskDefs) + return tasks + } + } + + Array.empty[Task] + } + + def stealTasks( + testClasses: Loose.Agg[ClassWithFingerprint], + testReporter: TestReporter, + runner: Runner, + stealFolder: os.Path, + testClassesFolder: os.Path + )(implicit ctx: Ctx.Log with Ctx.Home): (String, Iterator[TestResult]) = { + // Capture this value outside of the task event handler so it + // isn't affected by a test framework's stream redirects + val events = new ConcurrentLinkedQueue[Event]() + val globSelectorCache = testClasses.view.map { case (cls, fingerprint) => + cls.getName.stripSuffix("$") -> (cls, fingerprint) + }.toMap + while ({ + val tasks = + stealTaskFromTestClassesFolder(globSelectorCache, runner, stealFolder, testClassesFolder) + if (tasks.nonEmpty) { + executeTasks(tasks, testReporter, runner, events) + true + } else { + false + } + }) () + handleRunnerDone(runner, events) + } + + def stealTestFramework0( + frameworkInstances: ClassLoader => Framework, + testClassfilePath: Loose.Agg[Path], + args: Seq[String], + testClassesFolder: os.Path, + stealFolder: os.Path, + cl: ClassLoader, + testReporter: TestReporter + )(implicit ctx: Ctx.Log with Ctx.Home): (String, Seq[TestResult]) = { + + val framework = frameworkInstances(cl) + + val runner = framework.runner(args.toArray, Array[String](), cl) + + val testClasses = discoverTests(cl, framework, testClassfilePath) + + val (doneMessage, results) = + stealTasks(testClasses, testReporter, runner, stealFolder, testClassesFolder) + + (doneMessage, results.toSeq) + } + def getTestTasks0( frameworkInstances: ClassLoader => Framework, testClassfilePath: Loose.Agg[Path],