diff --git a/integration/feature/output-directory/resources/build.mill b/integration/feature/output-directory/resources/build.mill index b5fae4e0f89..e068bbd11cb 100644 --- a/integration/feature/output-directory/resources/build.mill +++ b/integration/feature/output-directory/resources/build.mill @@ -5,4 +5,16 @@ import mill.scalalib._ object `package` extends RootModule with ScalaModule { def scalaVersion = scala.util.Properties.versionNumberString + + def hello = Task { + "Hello from hello task" + } + + def blockWhileExists(path: os.Path) = Task.Command[String] { + if (!os.exists(path)) + os.write(path, Array.emptyByteArray) + while (os.exists(path)) + Thread.sleep(100L) + "Blocking command done" + } } diff --git a/integration/feature/output-directory/src/OutputDirectoryLockTests.scala b/integration/feature/output-directory/src/OutputDirectoryLockTests.scala new file mode 100644 index 00000000000..81b278d0879 --- /dev/null +++ b/integration/feature/output-directory/src/OutputDirectoryLockTests.scala @@ -0,0 +1,82 @@ +package mill.integration + +import mill.testkit.UtestIntegrationTestSuite +import utest._ + +import java.io.ByteArrayOutputStream +import java.util.concurrent.CountDownLatch + +import scala.concurrent.Await +import scala.concurrent.duration.Duration + +object OutputDirectoryLockTests extends UtestIntegrationTestSuite { + + def tests: Tests = Tests { + test("basic") - integrationTest { tester => + import tester._ + val signalFile = workspacePath / "do-wait" + System.err.println("Spawning blocking task") + val blocksFuture = evalAsync(("show", "blockWhileExists", "--path", signalFile), check = true) + while (!os.exists(signalFile) && !blocksFuture.isCompleted) + Thread.sleep(100L) + if (os.exists(signalFile)) + System.err.println("Blocking task is running") + else { + System.err.println("Failed to run blocking task") + Predef.assert(blocksFuture.isCompleted) + blocksFuture.value.get.get + } + + val testCommand: os.Shellable = ("show", "hello") + val testMessage = "Hello from hello task" + + System.err.println("Evaluating task without lock") + val noLockRes = eval(("--no-build-lock", testCommand), check = true) + assert(noLockRes.out.contains(testMessage)) + + System.err.println("Evaluating task without waiting for lock (should fail)") + val noWaitRes = eval(("--no-wait-for-build-lock", testCommand)) + assert(noWaitRes.err.contains("Cannot proceed, another Mill process is running tasks")) + + System.err.println("Evaluating task waiting for the lock") + + val lock = new CountDownLatch(1) + val stderr = new ByteArrayOutputStream + var success = false + val futureWaitingRes = evalAsync( + testCommand, + stderr = os.ProcessOutput { + val expectedMessage = + "Another Mill process is running tasks, waiting for it to be done..." + + (bytes, len) => + stderr.write(bytes, 0, len) + val output = new String(stderr.toByteArray) + if (output.contains(expectedMessage)) + lock.countDown() + }, + check = true + ) + try { + lock.await() + success = true + } finally { + if (!success) { + System.err.println("Waiting task output:") + System.err.write(stderr.toByteArray) + } + } + + System.err.println("Task is waiting for the lock, unblocking it") + os.remove(signalFile) + + System.err.println("Blocking task should exit") + val blockingRes = Await.result(blocksFuture, Duration.Inf) + assert(blockingRes.out.contains("Blocking command done")) + + System.err.println("Waiting task should be free to proceed") + val waitingRes = Await.result(futureWaitingRes, Duration.Inf) + assert(waitingRes.out.contains(testMessage)) + } + } +} diff --git a/testkit/src/mill/testkit/IntegrationTester.scala b/testkit/src/mill/testkit/IntegrationTester.scala index 043b530a5c5..a82958b94ab 100644 --- a/testkit/src/mill/testkit/IntegrationTester.scala +++ b/testkit/src/mill/testkit/IntegrationTester.scala @@ -5,6 +5,11 @@ import mill.eval.Evaluator import mill.resolve.SelectMode import ujson.Value +import java.util.concurrent.atomic.AtomicInteger + +import scala.concurrent.{Future, Promise} +import scala.util.Try + /** * Helper meant for executing Mill integration tests, which runs Mill in a subprocess * against a folder with a `build.mill` and project files. Provides APIs such as [[eval]] @@ -91,6 +96,49 @@ object IntegrationTester { ) } + private val evalAsyncCounter = new AtomicInteger + def evalAsync( + cmd: os.Shellable, + env: Map[String, String] = millTestSuiteEnv, + cwd: os.Path = workspacePath, + stdin: os.ProcessInput = os.Pipe, + stdout: os.ProcessOutput = os.Pipe, + stderr: os.ProcessOutput = os.Pipe, + mergeErrIntoOut: Boolean = false, + timeout: Long = -1, + check: Boolean = false, + propagateEnv: Boolean = true, + timeoutGracePeriod: Long = 100 + ): Future[IntegrationTester.EvalResult] = { + + val promise = Promise[IntegrationTester.EvalResult]() + + val thread = new Thread(s"mill-test-background-eval-${evalAsyncCounter.incrementAndGet()}") { + setDaemon(true) + override def run(): Unit = + promise.complete { + Try { + eval( + cmd = cmd, + env = env, + cwd = cwd, + stdin = stdin, + stdout = stdout, + stderr = stderr, + mergeErrIntoOut = mergeErrIntoOut, + timeout = timeout, + check = check, + propagateEnv = propagateEnv, + timeoutGracePeriod = timeoutGracePeriod + ) + } + } + } + thread.start() + + promise.future + } + def millTestSuiteEnv: Map[String, String] = Map("MILL_TEST_SUITE" -> this.getClass().toString()) /**