Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add API for spawning task-futures, use it for grouping and parallelization of test classes within a single module #3478

Merged
merged 46 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
29048c1
.
lihaoyi Sep 7, 2024
1d9fb57
wip
lihaoyi Sep 7, 2024
2503c31
wip
lihaoyi Sep 7, 2024
8bf7299
.
lihaoyi Sep 7, 2024
2b65cca
.
lihaoyi Sep 7, 2024
58a059e
.
lihaoyi Sep 7, 2024
68ec8e2
.
lihaoyi Sep 7, 2024
5fed0e3
.
lihaoyi Sep 7, 2024
c5372ad
.
lihaoyi Sep 7, 2024
7d76a7c
merge
lihaoyi Sep 8, 2024
62ac934
.
lihaoyi Sep 8, 2024
b507a1f
.
lihaoyi Sep 8, 2024
83a479e
.
lihaoyi Sep 8, 2024
e26c140
merge
lihaoyi Sep 17, 2024
4adb678
filtered test classes using selectors before forking
lihaoyi Sep 17, 2024
0fe0140
.
lihaoyi Sep 17, 2024
2cb2ff5
.
lihaoyi Sep 17, 2024
069c66b
.
lihaoyi Sep 17, 2024
9e7850b
wip
lihaoyi Sep 17, 2024
24432b6
.
lihaoyi Sep 17, 2024
bd4f778
fix
lihaoyi Sep 17, 2024
722db61
merge
lihaoyi Sep 18, 2024
6048490
put back final doesNotMatch check
lihaoyi Sep 18, 2024
34084cd
initial merge
lihaoyi Sep 26, 2024
615d57b
create simple example
lihaoyi Sep 26, 2024
2f33c1c
wip
lihaoyi Sep 26, 2024
28db0d3
kinda-works
lihaoyi Sep 26, 2024
8fe3d5a
wip
lihaoyi Sep 26, 2024
6939c74
wip
lihaoyi Sep 26, 2024
af38db8
.
lihaoyi Sep 26, 2024
51ca042
.
lihaoyi Sep 26, 2024
d4866f3
.
lihaoyi Sep 26, 2024
3433f03
.
lihaoyi Sep 26, 2024
e42d913
.
lihaoyi Sep 26, 2024
a3afc0c
.
lihaoyi Sep 27, 2024
866d08f
.
lihaoyi Sep 27, 2024
748c71f
Merge branch 'main' into test-par
lihaoyi Sep 28, 2024
10c29c3
.
lihaoyi Sep 28, 2024
4560377
.
lihaoyi Sep 28, 2024
2a4233f
.
lihaoyi Sep 30, 2024
56508c9
.
lihaoyi Sep 30, 2024
6d1bbc0
.
lihaoyi Sep 30, 2024
5bbf7ba
merge
lihaoyi Sep 30, 2024
e2c5422
.
lihaoyi Sep 30, 2024
0a9865c
.
lihaoyi Sep 30, 2024
4c30f76
.
lihaoyi Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,4 @@ jobs:
uses: ./.github/workflows/run-mill-action.yml
with:
java-version: '11'
buildcmd: ./mill -i mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources + __.mimaReportBinaryIssues + __.fix --check
buildcmd: ./mill -i mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources; ./mill -i __.mimaReportBinaryIssues; ./mill -i __.fix --check
2 changes: 1 addition & 1 deletion bsp/src/mill/bsp/BspContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private[mill] class BspContext(
override def info(s: String): Unit = streams.err.println(s)
override def error(s: String): Unit = streams.err.println(s)
override def ticker(s: String): Unit = streams.err.println(s)
override def ticker(key: String, s: String): Unit = streams.err.println(s)
override def setPromptDetail(key: Seq[String], s: String): Unit = streams.err.println(s)
override def debug(s: String): Unit = streams.err.println(s)

override def debugEnabled: Boolean = true
Expand Down
1 change: 1 addition & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ trait MillScalaModule extends ScalaModule with MillJavaModule with ScalafixModul
def moduleDeps = outer.testModuleDeps
def ivyDeps = super.ivyDeps() ++ outer.testIvyDeps()
def forkEnv = super.forkEnv() ++ outer.forkEnv()
// override def testForkGrouping = discoveredTestClasses().grouped(1).toSeq
Copy link
Member Author

Choose a reason for hiding this comment

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

We can turn this on properly once we re-bootstrap

}
}

Expand Down
5 changes: 5 additions & 0 deletions docs/modules/ROOT/pages/Tasks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ include::example/depth/tasks/4-inputs.adoc[]
include::example/depth/tasks/5-persistent-targets.adoc[]

=== Workers

include::example/depth/tasks/6-workers.adoc[]


== Using ScalaModule.run as a task

include::example/depth/tasks/11-module-run-task.adoc[]

== (Experimental) Forking Concurrent Futures within Tasks

include::example/depth/tasks/7-forking-futures.adoc[]
6 changes: 5 additions & 1 deletion docs/modules/ROOT/pages/Testing_Java_Projects.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ include::example/javalib/testing/2-test-deps.adoc[]

== Defining Integration Test Suites

include::example/javalib/testing/3-integration-suite.adoc[]
include::example/javalib/testing/3-integration-suite.adoc[]

== Test Grouping

include::example/javalib/testing/4-test-grouping.adoc[]
6 changes: 5 additions & 1 deletion docs/modules/ROOT/pages/Testing_Kotlin_Projects.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ include::example/kotlinlib/testing/2-test-deps.adoc[]

== Defining Integration Test Suites

include::example/kotlinlib/testing/3-integration-suite.adoc[]
include::example/kotlinlib/testing/3-integration-suite.adoc[]

== Test Grouping

include::example/kotlinlib/testing/4-test-grouping.adoc[]
6 changes: 5 additions & 1 deletion docs/modules/ROOT/pages/Testing_Scala_Projects.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ include::example/scalalib/testing/2-test-deps.adoc[]

== Defining Integration Test Suites

include::example/scalalib/testing/3-integration-suite.adoc[]
include::example/scalalib/testing/3-integration-suite.adoc[]

== Test Grouping

include::example/scalalib/testing/4-test-grouping.adoc[]
56 changes: 56 additions & 0 deletions example/depth/tasks/7-forking-futures/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Mill provides the `T.fork.async` and `T.fork.await` APIs for spawning async
// futures within a task and aggregating their results later:

package build

import mill._

def taskSpawningFutures = Task {
val f1 = T.fork.async(dest = T.dest / "future-1", key = "1", message = "First Future"){
println("Running First Future inside " + os.pwd)
Thread.sleep(3000)
val res = 1
println("Finished First Future")
res
}
val f2 = T.fork.async(dest = T.dest / "future-2", key = "2", message = "Second Future"){
println("Running Second Future inside " + os.pwd)
Thread.sleep(3000)
val res = 2
println("Finished Second Future")
res
}

T.fork.await(f1) + T.fork.await(f2)
}

/** Usage

> ./mill show taskSpawningFutures
[1] Running First Future inside .../out/taskSpawningFutures.dest/future-1
[2] Running Second Future inside .../out/taskSpawningFutures.dest/future-2
[1] Finished First Future
[2] Finished Second Future
3

*/


// `T.fork.async` takes several parameters in addition to the code block to be run:
//
// - `dest` is a folder for which the async future is to be run, overriding `os.pwd`
// for the duration of the future
// - `key` is a short prefix prepended to log lines to let you easily identify the future's
// log lines and distinguish them from logs of other futures and tasks running concurrently
// - `message` is a one-line description of what the future is doing
//
// Futures spawned by `T.fork.async` count towards Mill's `-j`/`--jobs` concurrency limit
// (which defaults to one-per-core), so you can freely use `T.fork.async` without worrying
// about spawning too many concurrent threads and causing CPU or memory contention. `T.fork`
// uses Java's built in `ForkJoinPool` and `ManagedBlocker` infrastructure under the hood
// to effectively manage the number of running threads.
//
// While `scala.concurrent` and `java.util.concurrent` can also be used to spawn thread
// pools and run async futures, `T.fork` provides a way to do so that integrates with Mill's
// existing concurrency, sandboxing and logging systems. Thus you should always prefer to
// run async futures on `T.fork` whenever possible.
20 changes: 20 additions & 0 deletions example/javalib/testing/4-test-grouping/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

//// 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().grouped(1).toSeq
}
}

/** See Also: foo/test/src/foo/HelloTests.java */
/** See Also: foo/test/src/foo/WorldTests.java */

//// SNIPPET:END
11 changes: 11 additions & 0 deletions example/javalib/testing/4-test-grouping/foo/src/foo/Foo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package foo;

public class Foo {
public static void main(String[] args) {
System.out.println(new Foo().hello());
}

public String hello() {
return "Hello World";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo;

import static org.junit.Assert.assertTrue;
import org.junit.Test;

public class HelloTests {

@Test
public void hello() throws Exception {
System.out.println("Testing Hello");
String result = new Foo().hello();
assertTrue(result.startsWith("Hello"));
Thread.sleep(1000);
System.out.println("Testing Hello Completed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package foo;

import static org.junit.Assert.assertTrue;
import org.junit.Test;

public class WorldTests {
@Test
public void world() throws Exception {
System.out.println("Testing World");
String result = new Foo().hello();
assertTrue(result.endsWith("World"));
Thread.sleep(1000);
System.out.println("Testing World Completed");
}
}
26 changes: 26 additions & 0 deletions example/kotlinlib/testing/4-test-grouping/build.mill
Original file line number Diff line number Diff line change
@@ -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 KotlinModuleTests {
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().grouped(1).toSeq
}
}

/** See Also: foo/test/src/foo/HelloTests.kt */
/** See Also: foo/test/src/foo/WorldTests.kt */

//// SNIPPET:END
9 changes: 9 additions & 0 deletions example/kotlinlib/testing/4-test-grouping/foo/src/foo/Foo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package foo

open class Foo {

fun hello(): String = "Hello World"

}

fun main(args: Array<String>) = println(Foo().hello())
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package foo

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldStartWith

class HelloTests : FunSpec({
test("hello") {
println("Testing Hello")
val result = Foo().hello()
result shouldStartWith "Hello"
java.lang.Thread.sleep(1000)
println("Testing Hello Completed")
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package foo

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldEndWith

class WorldTests : FunSpec({
test("world") {
println("Testing World")
val result = Foo().hello()
result shouldEndWith "World"
java.lang.Thread.sleep(1000)
println("Testing World Completed")
}
})
55 changes: 55 additions & 0 deletions example/scalalib/testing/4-test-grouping/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Test Grouping is an opt-in feature that allows you to take a single
// test module and group the test classes such that each group will
// execute in parallel when you call `test`. Test grouping is enabled
// by overriding `def testForkGrouping`, as shown 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.4")
def testFramework = "utest.runner.Framework"
def testForkGrouping = discoveredTestClasses().grouped(1).toSeq
}
}
/** See Also: foo/test/src/HelloTests.scala */
/** See Also: foo/test/src/WorldTests.scala */

//// SNIPPET:END

// In this example, we have one test module `foo.test`, and two test classes
// `HelloTests` and `WorldTests`. By default, all test classes in the same
// module run sequentially in the same JVM, but with `testForkGrouping` we can break up the
// module and run each test class in parallel in separate JVMs, each with their own
// separate `sandbox` folder and `.log` file:

/** Usage

> mill foo.test

> find out/foo/test/test.dest
...
out/foo/test/test.dest/foo.HelloTests.log
out/foo/test/test.dest/foo.HelloTests/sandbox
out/foo/test/test.dest/foo.WorldTests.log
out/foo/test/test.dest/foo.WorldTests/sandbox
out/foo/test/test.dest/test-report.xml

*/

// Test grouping allows you to run tests in parallel while keeping things deterministic and
// debuggable: parallel test groups will not write over each others files in their
// sandbox, and each one will have a separate set of logs that can be easily read
// without the others mixed in
//
// In this example, `def testForkGrouping = discoveredTestClasses().grouped(1).toSeq` assigns
// each test class to its own group, running in its own JVM. This comes with some overhead
// on a per-JVM basis, so if your test classes are numerous and small you may want to assign
// multiple test classes per group. You can also configure `testForkGrouping` to choose which
// test classes you want to run together and which to run alone: e.g. if some test classes
// are much slower than others, you may want to put the slow test classes each in its own
// group to reduce latency, while grouping multiple fast test classes together to reduce the
// per-group overhead of spinning up a separate JVM.
7 changes: 7 additions & 0 deletions example/scalalib/testing/4-test-grouping/foo/src/Foo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package foo
object Foo {
def main(args: Array[String]): Unit = {
println(hello())
}
def hello(): String = "Hello World"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package foo
import utest._
object HelloTests extends TestSuite {
def tests = Tests {
test("hello") {
println("Testing Hello")
val result = Foo.hello()
assert(result.startsWith("Hello"))
Thread.sleep(1000)
println("Testing Hello Completed")
result
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package foo
import utest._
object WorldTests extends TestSuite {
def tests = Tests {
test("world") {
println("Testing World")
val result = Foo.hello()
assert(result.endsWith("World"))
Thread.sleep(1000)
println("Testing World Completed")
result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ object WatchSourceInputTests extends UtestIntegrationTestSuite {
}
}
}
test("show") - retry(3) {
test("show") - /*retry(3) */ {
integrationTest { tester =>
if (!Util.isWindows) {
testWatchInput(tester, true)
Expand Down
Loading
Loading