From a34932ec0ab810eab1206afd2ef0fe59fa5362cc Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 25 Aug 2024 23:25:15 +0800 Subject: [PATCH] Try and simplify ExampleTester (#3419) Removes some special case and generalizes them into vanilla subprocess commands. Bash already exists, so rather than trying to write a poorly-defined custom DSL that is meant to look like bash and behave like bash, we just use bash. Need to figure out how to install `bash` on windows, but once that's done this lets us cut out a huge amount of cruft and complexity --- .github/workflows/actions.yml | 2 +- example/depth/cross/6-axes-extension/build.sc | 4 +- .../cross/9-dynamic-cross-modules/build.sc | 2 +- .../depth/tasks/5-persistent-targets/build.sc | 2 +- example/depth/tasks/6-workers/build.sc | 2 +- .../plugins/7-writing-mill-plugins/build.sc | 2 +- example/javalib/web/1-hello-jetty/build.sc | 2 +- .../javalib/web/2-hello-spring-boot/build.sc | 2 +- .../javalib/web/3-todo-spring-boot/build.sc | 2 +- .../javalib/web/4-hello-micronaut/build.sc | 2 +- example/javalib/web/5-todo-micronaut/build.sc | 2 +- .../basic/4-builtin-commands/build.sc | 4 +- .../scalalib/builds/1-common-config/build.sc | 2 +- .../module/11-assembly-config/build.sc | 4 +- .../module/3-run-compile-deps/build.sc | 2 +- .../testing/3-integration-suite/build.sc | 13 +- example/scalalib/web/1-todo-webapp/build.sc | 2 +- .../web/2-webapp-cache-busting/build.sc | 2 +- .../scalalib/web/4-webapp-scalajs/build.sc | 2 +- .../web/5-webapp-scalajs-shared/build.sc | 2 +- .../src/mill/testkit/ExampleTestSuite.scala | 2 +- testkit/src/mill/testkit/ExampleTester.scala | 207 +++++++----------- .../src/mill/testkit/IntegrationTester.scala | 28 +-- .../mill/testkit/IntegrationTesterBase.scala | 30 +++ .../example-test-example-project/build.sc | 2 +- 25 files changed, 130 insertions(+), 196 deletions(-) create mode 100644 testkit/src/mill/testkit/IntegrationTesterBase.scala diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 75b5eda432b..a2fecdaee51 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -148,7 +148,7 @@ jobs: - java-version: 17 millargs: '"scalajslib.__.test"' - java-version: 11 - millargs: '"example.scalalib.basic.__.local.test"' + millargs: '"example.scalalib.basic.__.fork.test"' - java-version: 17 millargs: "'integration.feature[_].fork.test'" - java-version: 11 diff --git a/example/depth/cross/6-axes-extension/build.sc b/example/depth/cross/6-axes-extension/build.sc index 81e557099b6..a8820b84943 100644 --- a/example/depth/cross/6-axes-extension/build.sc +++ b/example/depth/cross/6-axes-extension/build.sc @@ -49,9 +49,9 @@ trait FooModule3 extends FooModule2 with Cross.Module3[String, Int, Boolean] { > mill show foo3[b,2,false].param3 "Param Value: false" -> sed -i 's/, true//g' build.sc +> sed -i.bak 's/, true//g' build.sc -> sed -i 's/, false//g' build.sc +> sed -i.bak 's/, false//g' build.sc > mill show foo3[b,2,false].param3 error: ...object foo3 extends Cross[FooModule3](("a", 1), ("b", 2)) diff --git a/example/depth/cross/9-dynamic-cross-modules/build.sc b/example/depth/cross/9-dynamic-cross-modules/build.sc index 9f76d4ebca0..b074dfad2b0 100644 --- a/example/depth/cross/9-dynamic-cross-modules/build.sc +++ b/example/depth/cross/9-dynamic-cross-modules/build.sc @@ -33,7 +33,7 @@ error: Cannot resolve modules[new]... > cp -r modules/bar modules/new -> sed -i 's/Bar/New/g' modules/new/src/Example.scala +> sed -i.bak 's/Bar/New/g' modules/new/src/Example.scala > mill resolve modules[_] modules[bar] diff --git a/example/depth/tasks/5-persistent-targets/build.sc b/example/depth/tasks/5-persistent-targets/build.sc index 0844970fbba..a1e4f551a6f 100644 --- a/example/depth/tasks/5-persistent-targets/build.sc +++ b/example/depth/tasks/5-persistent-targets/build.sc @@ -68,7 +68,7 @@ Compressing: world.txt > ./mill compressedData # when no input changes, compressedData does not evaluate at all -> sed -i 's/Hello/HELLO/g' data/hello.txt +> sed -i.bak 's/Hello/HELLO/g' data/hello.txt > ./mill compressedData # when one input file changes, only that file is re-compressed Compressing: hello.txt diff --git a/example/depth/tasks/6-workers/build.sc b/example/depth/tasks/6-workers/build.sc index a0bfd059e79..6e11380537d 100644 --- a/example/depth/tasks/6-workers/build.sc +++ b/example/depth/tasks/6-workers/build.sc @@ -88,7 +88,7 @@ Compressing: world.txt > ./mill compressedData # when no input changes, compressedData does not evaluate at all -> sed -i 's/Hello/HELLO/g' data/hello.txt +> sed -i.bak 's/Hello/HELLO/g' data/hello.txt > ./mill compressedData # not --no-server, we read the data from memory Compressing: hello.txt diff --git a/example/extending/plugins/7-writing-mill-plugins/build.sc b/example/extending/plugins/7-writing-mill-plugins/build.sc index 6644e34c8ee..1d9d8d77654 100644 --- a/example/extending/plugins/7-writing-mill-plugins/build.sc +++ b/example/extending/plugins/7-writing-mill-plugins/build.sc @@ -173,7 +173,7 @@ compiling 1 Scala source... /** Usage -> sed -i 's/0.0.1/0.0.2/g' build.sc +> sed -i.bak 's/0.0.1/0.0.2/g' build.sc > ./mill myplugin.publishLocal Publishing Artifact(com.lihaoyi,myplugin_2.13,0.0.2) to ivy repo... diff --git a/example/javalib/web/1-hello-jetty/build.sc b/example/javalib/web/1-hello-jetty/build.sc index 307d540b3af..c9ee3c9271f 100644 --- a/example/javalib/web/1-hello-jetty/build.sc +++ b/example/javalib/web/1-hello-jetty/build.sc @@ -18,7 +18,7 @@ object hello extends RootModule with JavaModule { > mill test ...HelloJettyTest.testHelloJetty finished... -> mill runBackground +> mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8085 ...

Hello, World!

... diff --git a/example/javalib/web/2-hello-spring-boot/build.sc b/example/javalib/web/2-hello-spring-boot/build.sc index d337e75a05e..3b9f5d6834b 100644 --- a/example/javalib/web/2-hello-spring-boot/build.sc +++ b/example/javalib/web/2-hello-spring-boot/build.sc @@ -22,7 +22,7 @@ object hello extends RootModule with JavaModule { > mill test ...com.example.HelloSpringBootTest#shouldReturnDefaultMessage() finished... -> mill runBackground +> mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8086 ...

Hello, World!

... diff --git a/example/javalib/web/3-todo-spring-boot/build.sc b/example/javalib/web/3-todo-spring-boot/build.sc index 1e51c001d85..025d54b0530 100644 --- a/example/javalib/web/3-todo-spring-boot/build.sc +++ b/example/javalib/web/3-todo-spring-boot/build.sc @@ -60,7 +60,7 @@ object hello extends RootModule with JavaModule { ...com.example.TodomvcIntegrationTests#homePageLoads() finished... ...com.example.TodomvcIntegrationTests#addNewTodoItem() finished... -> mill test.runBackground +> mill test.runBackground; sleep 2 # give time for server to start > curl http://localhost:8087 ...

todos

... diff --git a/example/javalib/web/4-hello-micronaut/build.sc b/example/javalib/web/4-hello-micronaut/build.sc index 6d37e1a9688..814cc6fb4d6 100644 --- a/example/javalib/web/4-hello-micronaut/build.sc +++ b/example/javalib/web/4-hello-micronaut/build.sc @@ -68,7 +68,7 @@ trait MicronautModule extends MavenModule{ > mill test ...example.micronaut.HelloControllerTest#testHello()... -> mill runBackground +> mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8088/hello ...Hello World... diff --git a/example/javalib/web/5-todo-micronaut/build.sc b/example/javalib/web/5-todo-micronaut/build.sc index 77482a4b507..3d4e45c1bc0 100644 --- a/example/javalib/web/5-todo-micronaut/build.sc +++ b/example/javalib/web/5-todo-micronaut/build.sc @@ -89,7 +89,7 @@ trait MicronautModule extends MavenModule{ ...example.micronaut.TodoItemControllerTest... ...example.micronaut.HtmxWebJarsTest... -> mill runBackground +> mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8088 ...

todos

... diff --git a/example/scalalib/basic/4-builtin-commands/build.sc b/example/scalalib/basic/4-builtin-commands/build.sc index e6c03f910e9..c259084b7b8 100644 --- a/example/scalalib/basic/4-builtin-commands/build.sc +++ b/example/scalalib/basic/4-builtin-commands/build.sc @@ -159,7 +159,7 @@ Inputs: /** Usage -> mill show "foo.{sources,compileClasspath}" +> mill show 'foo.{sources,compileClasspath}' { "foo.sources": [ ".../foo/src" @@ -179,7 +179,7 @@ Inputs: /** Usage -> mill showNamed "foo.{sources,compileClasspath}" +> mill showNamed 'foo.{sources,compileClasspath}' { "foo.sources": [ ".../foo/src" diff --git a/example/scalalib/builds/1-common-config/build.sc b/example/scalalib/builds/1-common-config/build.sc index b68f4f26479..a4dc36c5283 100644 --- a/example/scalalib/builds/1-common-config/build.sc +++ b/example/scalalib/builds/1-common-config/build.sc @@ -117,7 +117,7 @@ my.custom.property: my-prop-value /** Usage -> sed -i 's/Foo2 {/Foo2 { println(this + "hello")/g' custom-src/Foo2.scala +> sed -i.bak 's/Foo2 {/Foo2 { println(this + "hello")/g' custom-src/Foo2.scala > mill compile # demonstrate -deprecation/-Xfatal-warnings flags error: object Foo2 { println(this + "hello") diff --git a/example/scalalib/module/11-assembly-config/build.sc b/example/scalalib/module/11-assembly-config/build.sc index 818e8c730bd..930ed593660 100644 --- a/example/scalalib/module/11-assembly-config/build.sc +++ b/example/scalalib/module/11-assembly-config/build.sc @@ -36,11 +36,11 @@ object bar extends ScalaModule { > ./mill foo.assembly -> unzip -p ./out/foo/assembly.dest/out.jar application.conf +> unzip -p ./out/foo/assembly.dest/out.jar application.conf || true Bar Application Conf Foo Application Conf -> java -jar ./out/foo/assembly.dest/out.jar\ +> java -jar ./out/foo/assembly.dest/out.jar Loaded application.conf from resources:... ...Foo Application Conf ...Bar Application Conf diff --git a/example/scalalib/module/3-run-compile-deps/build.sc b/example/scalalib/module/3-run-compile-deps/build.sc index 7c0c4eab6e2..7c990e53dd2 100644 --- a/example/scalalib/module/3-run-compile-deps/build.sc +++ b/example/scalalib/module/3-run-compile-deps/build.sc @@ -43,7 +43,7 @@ object bar extends ScalaModule { /** Usage -> ./mill foo.runBackground +> ./mill foo.runBackground; sleep 2 # give time for server to start > curl http://localhost:8079 Hello World! diff --git a/example/scalalib/testing/3-integration-suite/build.sc b/example/scalalib/testing/3-integration-suite/build.sc index 35138f839bf..28e1b457341 100644 --- a/example/scalalib/testing/3-integration-suite/build.sc +++ b/example/scalalib/testing/3-integration-suite/build.sc @@ -25,18 +25,7 @@ object qux extends ScalaModule { /** Usage -> mill qux.test # run the normal test suite -...qux.QuxTests...hello... -...qux.QuxTests...world... - - -> mill qux.integration # run the integration test suite -...qux.QuxIntegrationTests...helloworld... - -> mill qux.integration.testCached # run the normal test suite, caching the results -...qux.QuxIntegrationTests...helloworld... - -> mill qux.{test,integration} # run both test suites +> mill 'qux.{test,integration}' # run both test suites ...qux.QuxTests...hello... ...qux.QuxTests...world... ...qux.QuxIntegrationTests...helloworld... diff --git a/example/scalalib/web/1-todo-webapp/build.sc b/example/scalalib/web/1-todo-webapp/build.sc index f6b340531ea..3549d908265 100644 --- a/example/scalalib/web/1-todo-webapp/build.sc +++ b/example/scalalib/web/1-todo-webapp/build.sc @@ -26,7 +26,7 @@ object root extends RootModule with ScalaModule { > ./mill test + webapp.WebAppTests.simpleRequest... -> ./mill runBackground +> ./mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8080 ...What needs to be done... diff --git a/example/scalalib/web/2-webapp-cache-busting/build.sc b/example/scalalib/web/2-webapp-cache-busting/build.sc index 5f6f43ef4c0..a078d5a0c85 100644 --- a/example/scalalib/web/2-webapp-cache-busting/build.sc +++ b/example/scalalib/web/2-webapp-cache-busting/build.sc @@ -63,7 +63,7 @@ def hashFile(path: os.Path, src: os.Path, dest: os.Path) = { > ./mill test + webapp.WebAppTests.simpleRequest ... -> ./mill runBackground +> ./mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8081 ...What needs to be done... diff --git a/example/scalalib/web/4-webapp-scalajs/build.sc b/example/scalalib/web/4-webapp-scalajs/build.sc index a851f3a92e8..aeb515b3cf8 100644 --- a/example/scalalib/web/4-webapp-scalajs/build.sc +++ b/example/scalalib/web/4-webapp-scalajs/build.sc @@ -50,7 +50,7 @@ object root extends RootModule with ScalaModule { > ./mill test + webapp.WebAppTests.simpleRequest ... -> ./mill runBackground +> ./mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8082 ...What needs to be done... diff --git a/example/scalalib/web/5-webapp-scalajs-shared/build.sc b/example/scalalib/web/5-webapp-scalajs-shared/build.sc index 2052523f606..c7f70666570 100644 --- a/example/scalalib/web/5-webapp-scalajs-shared/build.sc +++ b/example/scalalib/web/5-webapp-scalajs-shared/build.sc @@ -68,7 +68,7 @@ object root extends RootModule with AppScalaModule { > ./mill test + webapp.WebAppTests.simpleRequest ... -> ./mill runBackground +> ./mill runBackground; sleep 2 # give time for server to start > curl http://localhost:8083 ...What needs to be done... diff --git a/testkit/src/mill/testkit/ExampleTestSuite.scala b/testkit/src/mill/testkit/ExampleTestSuite.scala index 94fe69d6953..1696cf408f6 100644 --- a/testkit/src/mill/testkit/ExampleTestSuite.scala +++ b/testkit/src/mill/testkit/ExampleTestSuite.scala @@ -5,7 +5,7 @@ object ExampleTestSuite extends IntegrationTestSuite { val tests: Tests = Tests { test("exampleTest") { - new ExampleTester(this).run() + new ExampleTester(clientServerMode, workspaceSourcePath, millExecutable).run() } } } diff --git a/testkit/src/mill/testkit/ExampleTester.scala b/testkit/src/mill/testkit/ExampleTester.scala index 335e0acee3e..6d129be796f 100644 --- a/testkit/src/mill/testkit/ExampleTester.scala +++ b/testkit/src/mill/testkit/ExampleTester.scala @@ -50,18 +50,34 @@ import scala.concurrent.duration.FiniteDuration * to each one. */ object ExampleTester { - def run(clientServerMode: Boolean, workspaceSourcePath: os.Path, millExecutable: os.Path): Unit = + def run( + clientServerMode: Boolean, + workspaceSourcePath: os.Path, + millExecutable: os.Path, + bashExecutable: String = defaultBashExecutable() + ): Unit = new ExampleTester( - new IntegrationTester( - clientServerMode, - workspaceSourcePath, - millExecutable - ) + clientServerMode, + workspaceSourcePath, + millExecutable, + bashExecutable ).run() + + def defaultBashExecutable(): String = { + if (!mill.main.client.Util.isWindows) "bash" + else "C:\\Program Files\\Git\\usr\\bin\\bash.exe" + } } -class ExampleTester(tester: IntegrationTester.Impl) { - tester.initWorkspace() +class ExampleTester( + clientServerMode: Boolean, + val workspaceSourcePath: os.Path, + millExecutable: os.Path, + bashExecutable: String = ExampleTester.defaultBashExecutable() +) extends IntegrationTesterBase { + initWorkspace() + + os.copy.over(millExecutable, workspacePath / "mill") val testTimeout: FiniteDuration = 5.minutes @@ -85,7 +101,7 @@ class ExampleTester(tester: IntegrationTester.Impl) { } - def processCommandBlock(workspaceRoot: os.Path, commandBlock: String): Unit = { + def processCommandBlock(commandBlock: String): Unit = { val commandBlockLines = commandBlock.linesIterator.toVector val expectedSnippets = commandBlockLines.tail @@ -97,126 +113,55 @@ class ExampleTester(tester: IntegrationTester.Impl) { val incorrectPlatform = (comment.exists(_.startsWith("windows")) && !Util.windowsPlatform) || (comment.exists(_.startsWith("mac/linux")) && Util.windowsPlatform) || - (comment.exists(_.startsWith("--no-server")) && tester.clientServerMode) || - (comment.exists(_.startsWith("not --no-server")) && !tester.clientServerMode) + (comment.exists(_.startsWith("--no-server")) && clientServerMode) || + (comment.exists(_.startsWith("not --no-server")) && !clientServerMode) if (!incorrectPlatform) { - processCommand(workspaceRoot, expectedSnippets, commandHead.trim) + processCommand(expectedSnippets, commandHead.trim) } } def processCommand( - workspaceRoot: os.Path, expectedSnippets: Vector[String], - commandStr: String + commandStr0: String, + check: Boolean = true ): Unit = { - val cmd = BashTokenizer.tokenize(commandStr) - Console.err.println( - s"""$workspaceRoot> ${cmd.mkString("'", "' '", "'")} - |--- Expected output -------- - |${expectedSnippets.mkString("\n")} - |----------------------------""".stripMargin + val commandStr = commandStr0 match { + case s"mill $rest" => s"./mill $rest" + case s => s + } + Console.err.println(s"$workspacePath> $commandStr") + + val res = os.call( + (bashExecutable, "-c", commandStr), + stdout = os.Pipe, + stderr = os.Pipe, + cwd = workspacePath, + mergeErrIntoOut = true, + env = Map("MILL_TEST_SUITE" -> this.getClass().toString()), + check = false ) - cmd match { - case Seq("cp", "-r", from, to) => - os.copy(os.Path(from, workspaceRoot), os.Path(to, workspaceRoot)) - - case Seq("sed", "-i", s"s/$oldStr/$newStr/g", file) => - tester.modifyFile(os.Path(file, workspaceRoot), _.replace(oldStr, newStr)) - - case Seq("curl", url) => - Thread.sleep(1500) // Need to give backgroundWrapper time to spin up - val res = requests.get(url) - validateEval( - expectedSnippets, - IntegrationTester.EvalResult(res.is2xx, res.text(), "") - ) - - case Seq("cat", path) => - val res = os.read(os.Path(path, workspaceRoot)) - validateEval( - expectedSnippets, - IntegrationTester.EvalResult(true, res, "") - ) - - case Seq("node", rest @ _*) => - val res = os - .proc("node", rest) - .call(stdout = os.Pipe, stderr = os.Pipe, cwd = workspaceRoot) - validateEval( - expectedSnippets, - IntegrationTester.EvalResult(res.exitCode == 0, res.out.text(), res.err.text()) - ) - - case Seq("git", rest @ _*) => - val res = os - .proc("git", rest) - .call(stdout = os.Pipe, stderr = os.Pipe, cwd = workspaceRoot) - validateEval( - expectedSnippets, - IntegrationTester.EvalResult(res.exitCode == 0, res.out.text(), res.err.text()) - ) - - case Seq("java", "-jar", rest @ _*) => - val res = os - .proc("java", "-jar", rest) - .call(stdout = os.Pipe, stderr = os.Pipe, cwd = workspaceRoot) - validateEval( - expectedSnippets, - IntegrationTester.EvalResult(res.exitCode == 0, res.out.text(), res.err.text()) - ) - - case Seq("unzip", "-p", zip, path) => - val zipFile = new java.util.zip.ZipFile((workspaceRoot / os.SubPath(zip)).toIO) - try { - val boas = new java.io.ByteArrayOutputStream - os.Internals.transfer(zipFile.getInputStream(zipFile.getEntry(path)), boas) - validateEval( - expectedSnippets, - IntegrationTester.EvalResult(true, boas.toString("UTF-8"), "") - ) - } finally { - zipFile.close() - } - - case Seq("printf", literal, ">>", path) => - tester.modifyFile( - os.Path(path, tester.workspacePath), - _ + ujson.read(s""""${literal}"""").str - ) - - case Seq(command, rest @ _*) => - val evalResult = command match { - case "./mill" | "mill" => tester.eval(rest) - case s"./$cmd" => - val tokens = cmd +: rest - val executable = workspaceRoot / os.SubPath(tokens.head) - if (!os.exists(executable)) { - throw new Exception( - s"Executable $executable not found.\n" + - s"Other files present include ${os.list(executable / os.up)}" - ) - } - val res = os.call( - cmd = (executable, tokens.tail), - stdout = os.Pipe, - stderr = os.Pipe, - cwd = workspaceRoot - ) - - IntegrationTester.EvalResult(res.exitCode == 0, res.out.text(), res.err.text()) - } - validateEval(expectedSnippets, evalResult) - } + validateEval( + expectedSnippets, + IntegrationTester.EvalResult( + res.exitCode == 0, + fansi.Str(res.out.text(), errorMode = fansi.ErrorMode.Strip).plainText, + fansi.Str(res.err.text(), errorMode = fansi.ErrorMode.Strip).plainText + ), + check + ) } def validateEval( expectedSnippets: Vector[String], - evalResult: IntegrationTester.EvalResult + evalResult: IntegrationTester.EvalResult, + check: Boolean = true ): Unit = { - if (expectedSnippets.exists(_.startsWith("error: "))) assert(!evalResult.isSuccess) - else assert(evalResult.isSuccess) + if (check) { + if (expectedSnippets.exists(_.startsWith("error: "))) assert(!evalResult.isSuccess) + else assert(evalResult.isSuccess) + } val unwrappedExpected = expectedSnippets .map { @@ -238,32 +183,23 @@ class ExampleTester(tester: IntegrationTester.Impl) { ) .toVector - val filteredErr = plainTextLines(evalResult.err) val filteredOut = plainTextLines(evalResult.out) if (expectedSnippets.nonEmpty) { for (outLine <- filteredOut) { globMatchesAny(unwrappedExpected, outLine) } - for (errLine <- filteredErr) { - globMatchesAny(unwrappedExpected, errLine) - } } for (expectedLine <- unwrappedExpected.linesIterator) { - val combinedOutErr = (filteredOut ++ filteredErr).mkString("\n") - assert(globMatches(expectedLine, combinedOutErr)) + assert(filteredOut.exists(globMatches(expectedLine, _))) } } - def globMatches(expected: String, filtered: String): Boolean = { - filtered - .linesIterator - .exists( - StringContext - .glob(expected.split("\\.\\.\\.", -1).toIndexedSeq, _) - .nonEmpty - ) + def globMatches(expected: String, line: String): Boolean = { + StringContext + .glob(expected.split("\\.\\.\\.", -1).toIndexedSeq, line) + .nonEmpty } def globMatchesAny(expected: String, filtered: String): Boolean = { @@ -271,16 +207,21 @@ class ExampleTester(tester: IntegrationTester.Impl) { } def run(): Any = { - val parsed = ExampleParser(tester.workspaceSourcePath) + val parsed = ExampleParser(workspaceSourcePath) val usageComment = parsed.collect { case ("example", txt) => txt }.mkString("\n\n") val commandBlocks = ("\n" + usageComment.trim).split("\n> ").filter(_.nonEmpty) retryOnTimeout(3) { - try os.remove.all(tester.workspacePath / "out") - catch { case e: Throwable => /*do nothing*/ } + try { + try os.remove.all(workspacePath / "out") + catch { + case e: Throwable => /*do nothing*/ + } - for (commandBlock <- commandBlocks) processCommandBlock(tester.workspacePath, commandBlock) - if (tester.clientServerMode) tester.eval("shutdown") + for (commandBlock <- commandBlocks) processCommandBlock(commandBlock) + } finally { + if (clientServerMode) processCommand(Vector(), "./mill shutdown", check = false) + } } } } diff --git a/testkit/src/mill/testkit/IntegrationTester.scala b/testkit/src/mill/testkit/IntegrationTester.scala index b3f1b3addba..6557ec398ed 100644 --- a/testkit/src/mill/testkit/IntegrationTester.scala +++ b/testkit/src/mill/testkit/IntegrationTester.scala @@ -36,26 +36,13 @@ object IntegrationTester { */ case class EvalResult(isSuccess: Boolean, out: String, err: String) - trait Impl extends AutoCloseable { + trait Impl extends AutoCloseable with IntegrationTesterBase { def millExecutable: os.Path def workspaceSourcePath: os.Path val clientServerMode: Boolean - private val workspacePathBase = os.pwd / "out" / "interation-tester-workdir" - os.makeDir.all(workspacePathBase) - - /** - * The working directory of the integration test suite, which is the root of the - * Mill build being tested. Contains the `build.sc` file, any application code, and - * the `out/` folder containing the build output - * - * Make sure it lives inside `os.pwd` because somehow the tests fail on windows - * if it lives in the global temp folder. - */ - val workspacePath: os.Path = os.temp.dir(workspacePathBase, deleteOnExit = false) - def debugLog = false /** @@ -141,19 +128,6 @@ object IntegrationTester { def value[T: upickle.default.Reader]: T = upickle.default.read[T](cached.value) } - /** - * Initializes the workspace in preparation for integration testing - */ - def initWorkspace(): Unit = { - println(s"Copying integration test sources from $workspaceSourcePath to $workspacePath") - os.remove.all(workspacePath) - os.makeDir.all(workspacePath / os.up) - // somehow os.copy does not properly preserve symlinks - // os.copy(scriptSourcePath, workspacePath) - os.call(("cp", "-R", workspaceSourcePath, workspacePath)) - os.remove.all(workspacePath / "out") - } - /** * Helper method to modify a file containing text during your test suite. */ diff --git a/testkit/src/mill/testkit/IntegrationTesterBase.scala b/testkit/src/mill/testkit/IntegrationTesterBase.scala new file mode 100644 index 00000000000..b1eacab3284 --- /dev/null +++ b/testkit/src/mill/testkit/IntegrationTesterBase.scala @@ -0,0 +1,30 @@ +package mill.testkit + +trait IntegrationTesterBase { + def workspaceSourcePath: os.Path + private val workspacePathBase = os.pwd / "out" / "interation-tester-workdir" + os.makeDir.all(workspacePathBase) + + /** + * The working directory of the integration test suite, which is the root of the + * Mill build being tested. Contains the `build.sc` file, any application code, and + * the `out/` folder containing the build output + * + * Make sure it lives inside `os.pwd` because somehow the tests fail on windows + * if it lives in the global temp folder. + */ + val workspacePath: os.Path = os.temp.dir(workspacePathBase, deleteOnExit = false) + + /** + * Initializes the workspace in preparation for integration testing + */ + def initWorkspace(): Unit = { + println(s"Copying integration test sources from $workspaceSourcePath to $workspacePath") + os.remove.all(workspacePath) + os.makeDir.all(workspacePath / os.up) + // somehow os.copy does not properly preserve symlinks + // os.copy(scriptSourcePath, workspacePath) + os.call(("cp", "-R", workspaceSourcePath, workspacePath)) + os.remove.all(workspacePath / "out") + } +} diff --git a/testkit/test/resources/example-test-example-project/build.sc b/testkit/test/resources/example-test-example-project/build.sc index 646c44a0839..e6454ad9954 100644 --- a/testkit/test/resources/example-test-example-project/build.sc +++ b/testkit/test/resources/example-test-example-project/build.sc @@ -11,7 +11,7 @@ def testTask = T { os.read(testSource().path).toUpperCase() } > cat out/testTask.json ..."HELLO WORLD SOURCE FILE"... -> sed -i 's/file/file!!!/g' source-file.txt +> sed -i.bak 's/file/file!!!/g' source-file.txt > ./mill testTask