From 056e76eb9f0efaadded59baafd4c4a17f5925076 Mon Sep 17 00:00:00 2001 From: 0xnm <0xnm@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:01:59 +0200 Subject: [PATCH] Add test reporter for Kotlin/JS --- .../kotlinlib/web/3-hello-kotlinjs/build.mill | 2 +- .../kotlinlib/KotlinWorkerManagerImpl.scala | 2 +- .../mill/kotlinlib/js/KotlinJSModule.scala | 223 ++++++++++++++---- .../contrib/ktfmt/KtfmtModuleTests.scala | 2 +- .../js/KotlinJSKotestModuleTests.scala | 38 ++- ...KotlinJSKotlinTestPackageModuleTests.scala | 36 ++- .../mill/kotlinlib/js/KotlinJSLinkTests.scala | 2 - main/util/src/mill/util/Jvm.scala | 13 +- scalalib/src/mill/scalalib/JavaModule.scala | 2 +- .../scalalib/publish/SonatypeHelpers.scala | 2 +- .../src/mill/scalalib/AssemblyTestUtils.scala | 4 +- .../scalanativelib/ScalaNativeModule.scala | 3 +- 12 files changed, 250 insertions(+), 79 deletions(-) diff --git a/example/kotlinlib/web/3-hello-kotlinjs/build.mill b/example/kotlinlib/web/3-hello-kotlinjs/build.mill index 800fb8db491..bf284a6fd79 100644 --- a/example/kotlinlib/web/3-hello-kotlinjs/build.mill +++ b/example/kotlinlib/web/3-hello-kotlinjs/build.mill @@ -33,7 +33,7 @@ Compiling 1 Kotlin sources to .../out/test/compile.dest/classes... Linking IR to .../out/test/linkBinary.dest/binaries produce executable: .../out/test/linkBinary.dest/binaries ... -error: ...AssertionFailedError: expected:<"

Hello World Wrong

"> but was:<"

Hello World

... +error: ... expected:<"

Hello World Wrong

"> but was:<"

Hello World

... ... > cat out/test/linkBinary.dest/binaries/test.js # Generated javascript on disk diff --git a/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala b/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala index 5cfc77c10d6..d55e600e3c4 100644 --- a/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala +++ b/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala @@ -1,7 +1,7 @@ /* * Original code copied from https://github.com/lefou/mill-kotlin * Original code published under the Apache License Version 2 - * Original Copyright 2020-20 24 Tobias Roeser + * Original Copyright 2020-2024 Tobias Roeser */ package mill.kotlinlib diff --git a/kotlinlib/src/mill/kotlinlib/js/KotlinJSModule.scala b/kotlinlib/src/mill/kotlinlib/js/KotlinJSModule.scala index f0a46df5070..0e8934a2788 100644 --- a/kotlinlib/src/mill/kotlinlib/js/KotlinJSModule.scala +++ b/kotlinlib/src/mill/kotlinlib/js/KotlinJSModule.scala @@ -10,10 +10,12 @@ import mill.scalalib.api.CompilationResult import mill.testrunner.TestResult import mill.util.Jvm import mill.{Agg, Args, T} +import sbt.testing.Status import upickle.default.{macroRW, ReadWriter => RW} -import java.io.File +import java.io.{File, FileNotFoundException} import java.util.zip.ZipFile +import scala.xml.XML /** * This module is very experimental. Don't use it, it is still under the development, APIs can change. @@ -105,54 +107,69 @@ trait KotlinJSModule extends KotlinModule { outer => Task.Command { run(args)() } override def run(args: Task[Args] = Task.Anon(Args())): Command[Unit] = Task.Command { - val binaryKind = kotlinJSBinaryKind() - if (binaryKind.isEmpty || binaryKind.get != BinaryKind.Executable) { - T.log.error("Run action is only allowed for the executable binary") + runJsBinary( + args = args(), + binaryKind = kotlinJSBinaryKind(), + moduleKind = moduleKind(), + binaryDir = linkBinary().classes.path, + runTarget = kotlinJSRunTarget(), + envArgs = T.env, + workingDir = T.dest + ).getOrThrow + () + } + + override def runMainLocal( + @arg(positional = true) mainClass: String, + args: String* + ): Command[Unit] = Task.Command[Unit] { + mill.api.Result.Failure("runMain is not supported in Kotlin/JS.") + } + + override def runMain(@arg(positional = true) mainClass: String, args: String*): Command[Unit] = + Task.Command[Unit] { + mill.api.Result.Failure("runMain is not supported in Kotlin/JS.") } - val moduleKind = this.moduleKind() + protected[js] def runJsBinary( + args: Args = Args(), + binaryKind: Option[BinaryKind], + moduleKind: ModuleKind, + binaryDir: os.Path, + runTarget: Option[RunTarget], + envArgs: Map[String, String] = Map.empty[String, String], + workingDir: os.Path + )(implicit ctx: mill.api.Ctx): Result[Int] = { + if (binaryKind.isEmpty || binaryKind.get != BinaryKind.Executable) { + return Result.Failure("Run action is only allowed for the executable binary") + } - val linkResult = linkBinary().classes if ( moduleKind == ModuleKind.NoModule && - linkResult.path.toIO.listFiles().count(_.getName.endsWith(".js")) > 1 + binaryDir.toIO.listFiles().count(_.getName.endsWith(".js")) > 1 ) { T.log.info("No module type is selected for the executable, but multiple .js files found in the output folder." + " This will probably lead to the dependency resolution failure.") } - kotlinJSRunTarget() match { - case Some(RunTarget.Node) => { - val testBinaryPath = (linkResult.path / s"${moduleName()}.${moduleKind.extension}") + runTarget match { + case Some(RunTarget.Node) => + val binaryPath = (binaryDir / s"${moduleName()}.${moduleKind.extension}") .toIO.getAbsolutePath Jvm.runSubprocess( commandArgs = Seq( "node" - ) ++ args().value ++ Seq(testBinaryPath), - envArgs = T.env, - workingDir = T.dest + ) ++ args.value ++ Seq(binaryPath), + envArgs = envArgs, + workingDir = workingDir ) - } case Some(x) => - T.log.error(s"Run target $x is not supported") + Result.Failure(s"Run target $x is not supported") case None => - throw new IllegalArgumentException("Executable binary should have a run target selected.") + Result.Failure("Executable binary should have a run target selected.") } - - } - - override def runMainLocal( - @arg(positional = true) mainClass: String, - args: String* - ): Command[Unit] = Task.Command[Unit] { - mill.api.Result.Failure("runMain is not supported in Kotlin/JS.") } - override def runMain(@arg(positional = true) mainClass: String, args: String*): Command[Unit] = - Task.Command[Unit] { - mill.api.Result.Failure("runMain is not supported in Kotlin/JS.") - } - /** * The actual Kotlin compile task (used by [[compile]] and [[kotlincHelp]]). */ @@ -386,8 +403,8 @@ trait KotlinJSModule extends KotlinModule { outer => } } - private def moduleName() = fullModuleNameSegments().last - private def fullModuleName() = fullModuleNameSegments().mkString("-") + protected[js] def moduleName(): String = fullModuleNameSegments().last + protected[js] def fullModuleName(): String = fullModuleNameSegments().mkString("-") // **NOTE**: This logic may (and probably is) be incomplete private def isKotlinJsLibrary(path: os.Path)(implicit ctx: mill.api.Ctx): Boolean = { @@ -422,6 +439,8 @@ trait KotlinJSModule extends KotlinModule { outer => */ trait KotlinJSTests extends KotlinTests with KotlinJSModule { + private val defaultXmlReportName = "test-report.xml" + // region private // TODO may be optimized if there is a single folder for all modules @@ -439,7 +458,7 @@ trait KotlinJSModule extends KotlinModule { outer => commandArgs = Seq("npm", "install", "mocha@10.2.0"), envArgs = T.env, workingDir = workingDir - ) + ).getOrThrow PathRef(workingDir / "node_modules" / "mocha" / "bin" / "mocha.js") } @@ -448,8 +467,8 @@ trait KotlinJSModule extends KotlinModule { outer => Jvm.runSubprocess( commandArgs = Seq("npm", "install", "source-map-support@0.5.21"), envArgs = T.env, - workingDir = nodeModulesDir().path - ) + workingDir = workingDir + ).getOrThrow PathRef(workingDir / "node_modules" / "source-map-support" / "register.js") } @@ -457,7 +476,9 @@ trait KotlinJSModule extends KotlinModule { outer => override def testFramework = "" - override def kotlinJSBinaryKind: T[Option[BinaryKind]] = Some(BinaryKind.Executable) + override def kotlinJSRunTarget: T[Option[RunTarget]] = outer.kotlinJSRunTarget() + + override def moduleKind: T[ModuleKind] = ModuleKind.PlainModule override def splitPerModule = false @@ -470,20 +491,136 @@ trait KotlinJSModule extends KotlinModule { outer => args: Task[Seq[String]], globSelectors: Task[Seq[String]] ): Task[(String, Seq[TestResult])] = Task.Anon { - // This is a terrible hack, but it works - run(Task.Anon { - Args(args() ++ Seq( + runJsBinary( + // TODO add runner to be able to use test selector + args = Args(args() ++ Seq( // TODO this is valid only for the NodeJS target. Once browser support is // added, need to have different argument handling "--require", sourceMapSupportModule().path.toString(), - mochaModule().path.toString() - )) - })() - ("", Seq.empty[TestResult]) + mochaModule().path.toString(), + "--reporter", + "xunit", + "--reporter-option", + s"output=${testReportXml().getOrElse(defaultXmlReportName)}" + )), + binaryKind = Some(BinaryKind.Executable), + moduleKind = moduleKind(), + binaryDir = linkBinary().classes.path, + runTarget = kotlinJSRunTarget(), + envArgs = T.env, + workingDir = T.dest + ) + + // we don't care about the result returned above (because node will return exit code = 1 when tests fail), what + // matters is if test results file exists + val xmlReportName = testReportXml().getOrElse(defaultXmlReportName) + val xmlReportPath = T.dest / xmlReportName + val testResults = parseTestResults(xmlReportPath) + val totalCount = testResults.length + val passedCount = testResults.count(_.status == Status.Success.name()) + val failedCount = testResults.count(_.status == Status.Failure.name()) + val skippedCount = testResults.count(_.status == Status.Skipped.name()) + val doneMessage = + s""" + |Tests: $totalCount, Passed: $passedCount, Failed: $failedCount, Skipped: $skippedCount + | + |Full report is available at $xmlReportPath + |""".stripMargin + + if (failedCount != 0) { + val failedTests = testResults + .filter(_.status == Status.Failure.name()) + .map(result => + if (result.exceptionName.isEmpty && result.exceptionMsg.isEmpty) { + s"${result.fullyQualifiedName} - ${result.selector}" + } else { + s"${result.fullyQualifiedName} - ${result.selector}: ${result.exceptionName.getOrElse("<>")}:" + + s" ${result.exceptionMsg.getOrElse("<>")}" + } + ) + val failureMessage = + s""" + |Tests failed: + | + |${failedTests.mkString("\n")} + | + |""".stripMargin + Result.Failure(failureMessage, Some((doneMessage, testResults))) + } else { + Result.Success((doneMessage, testResults)) + } + } + + private def parseTestResults(path: os.Path): Seq[TestResult] = { + if (!os.exists(path)) { + throw new FileNotFoundException(s"Test results file $path wasn't found") + } + val xml = XML.loadFile(path.toIO) + (xml \ "testcase") + .map { node => + val (testStatus, exceptionName, exceptionMessage, exceptionTrace) = + if (node.child.exists(_.label == "failure")) { + val content = (node \ "failure") + .head + .child + .filter(_.isAtom) + .text + val lines = content.split("\n") + val exceptionMessage = lines.head + val exceptionType = lines(1).splitAt(lines(1).indexOf(":"))._1 + val trace = parseTrace(lines.drop(2)) + (Status.Failure, Some(exceptionType), Some(exceptionMessage), Some(trace)) + } else if (node.child.exists(_.label == "skipped")) { + (Status.Skipped, None, None, None) + } else { + (Status.Success, None, None, None) + } + + TestResult( + fullyQualifiedName = node \@ "classname", + selector = node \@ "name", + // probably in milliseconds? + duration = ((node \@ "time").toDouble * 1000).toLong, + status = testStatus.name(), + exceptionName = exceptionName, + exceptionMsg = exceptionMessage, + exceptionTrace = exceptionTrace + ) + } } - override def kotlinJSRunTarget: T[Option[RunTarget]] = Some(RunTarget.Node) + private def parseTrace(trace: Seq[String]): Seq[StackTraceElement] = { + trace.map { line => + // there are some lines with methods like this: $interceptCOROUTINE$97.l [as test_1], no idea what is this. + val strippedLine = line.trim.stripPrefix("at ") + val (symbol, location) = strippedLine.splitAt(strippedLine.lastIndexOf("(")) + // symbol can be like that HelloTests$_init_$lambda$slambda_wolooq_1.protoOf.doResume_5yljmg_k$ + // assume that everything past first dot is a method name, and everything before - some synthetic class name + // this may be completely wrong though, but at least location will be right + val (declaringClass, method) = if (symbol.contains(".")) { + symbol.splitAt(symbol.indexOf(".")) + } else { + ("", symbol) + } + // can be what we expect in case if line is pure-JVM: + // src/internal/JSDispatcher.kt:127:25 + // but can also be something like: + // node:internal/process/task_queues:77:11 + // drop closing ), then after split drop position on the line + val locationElements = location.dropRight(1).split(":").dropRight(1) + if (locationElements.length >= 2) { + new StackTraceElement( + declaringClass, + method, + locationElements(locationElements.length - 2), + locationElements.last.toInt + ) + } else { + new StackTraceElement(declaringClass, method, "", 0) + } + } + } } /** diff --git a/kotlinlib/test/src/mill/kotlinlib/contrib/ktfmt/KtfmtModuleTests.scala b/kotlinlib/test/src/mill/kotlinlib/contrib/ktfmt/KtfmtModuleTests.scala index 75fbad0890b..b1a8afe2c62 100644 --- a/kotlinlib/test/src/mill/kotlinlib/contrib/ktfmt/KtfmtModuleTests.scala +++ b/kotlinlib/test/src/mill/kotlinlib/contrib/ktfmt/KtfmtModuleTests.scala @@ -1,6 +1,6 @@ package mill.kotlinlib.ktfmt -import mill.{PathRef, T, Task, api} +import mill.{PathRef, T, api} import mill.kotlinlib.KotlinModule import mill.main.Tasks import mill.testkit.{TestBaseModule, UnitTester} diff --git a/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotestModuleTests.scala b/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotestModuleTests.scala index b9327e55576..77d00778edf 100644 --- a/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotestModuleTests.scala +++ b/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotestModuleTests.scala @@ -1,9 +1,11 @@ package mill package kotlinlib.js +import mill.api.Result import mill.eval.EvaluatorPaths import mill.testkit.{TestBaseModule, UnitTester} -import utest.{assert, TestSuite, Tests, test} +import sbt.testing.Status +import utest.{TestSuite, Tests, assert, test} object KotlinJSKotestModuleTests extends TestSuite { @@ -19,6 +21,7 @@ object KotlinJSKotestModuleTests extends TestSuite { object foo extends KotlinJSModule { def kotlinVersion = KotlinJSKotestModuleTests.kotlinVersion + override def kotlinJSRunTarget = Some(RunTarget.Node) override def moduleDeps = Seq(module.bar) object test extends KotlinJSModule with KotestTests { @@ -36,19 +39,30 @@ object KotlinJSKotestModuleTests extends TestSuite { val eval = testEval() val command = module.foo.test.test() - val Left(_) = eval.apply(command) + val Left(Result.Failure(failureMessage, Some((doneMessage, testResults)))) = + eval.apply(command) + + val xmlReport = + EvaluatorPaths.resolveDestPaths(eval.outPath, command).dest / "test-report.xml" - // temporary, because we are running run() task, it won't be test.log, but run.log - val log = - os.read(EvaluatorPaths.resolveDestPaths(eval.outPath, command).log / ".." / "run.log") assert( - log.contains( - "AssertionFailedError: expected:<\"Not hello, world\"> but was:<\"Hello, world\">" - ), - log.contains("1 passing"), - log.contains("1 failing"), - // verify that source map is applied, otherwise all stack entries will point to .js - log.contains("HelloKotestTests.kt:") + os.exists(xmlReport), + os.read(xmlReport).contains("HelloKotestTests.kt:"), + failureMessage == s""" + |Tests failed: + | + |HelloTests - failure: AssertionFailedError: expected:<\"Not hello, world\"> but was:<\"Hello, world\"> + | + |""".stripMargin, + doneMessage == s""" + |Tests: 2, Passed: 1, Failed: 1, Skipped: 0 + | + |Full report is available at $xmlReport + |""".stripMargin, + testResults.length == 2, + testResults.count(result => + result.status == Status.Failure.name() && result.exceptionTrace.getOrElse(Seq.empty).isEmpty + ) == 0 ) } } diff --git a/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotlinTestPackageModuleTests.scala b/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotlinTestPackageModuleTests.scala index fdd9b2039a0..84c9295b8f8 100644 --- a/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotlinTestPackageModuleTests.scala +++ b/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotlinTestPackageModuleTests.scala @@ -2,9 +2,11 @@ package mill package kotlinlib package js +import mill.api.Result import mill.eval.EvaluatorPaths import mill.testkit.{TestBaseModule, UnitTester} -import utest.{assert, TestSuite, Tests, test} +import sbt.testing.Status +import utest.{TestSuite, Tests, assert, test} object KotlinJSKotlinTestPackageModuleTests extends TestSuite { @@ -20,6 +22,7 @@ object KotlinJSKotlinTestPackageModuleTests extends TestSuite { object foo extends KotlinJSModule { def kotlinVersion = KotlinJSKotlinTestPackageModuleTests.kotlinVersion + override def kotlinJSRunTarget = Some(RunTarget.Node) override def moduleDeps = Seq(module.bar) object test extends KotlinJSModule with KotlinTestPackageTests { @@ -37,17 +40,30 @@ object KotlinJSKotlinTestPackageModuleTests extends TestSuite { val eval = testEval() val command = module.foo.test.test() - val Left(_) = eval.apply(command) + val Left(Result.Failure(failureMessage, Some((doneMessage, testResults)))) = + eval.apply(command) + + val xmlReport = + EvaluatorPaths.resolveDestPaths(eval.outPath, command).dest / "test-report.xml" - // temporary, because we are running run() task, it won't be test.log, but run.log - val log = - os.read(EvaluatorPaths.resolveDestPaths(eval.outPath, command).log / ".." / "run.log") assert( - log.contains("AssertionError: Expected , actual ."), - log.contains("1 passing"), - log.contains("1 failing"), - // verify that source map is applied, otherwise all stack entries will point to .js - log.contains("HelloKotlinTestPackageTests.kt:") + os.exists(xmlReport), + os.read(xmlReport).contains("HelloKotlinTestPackageTests.kt:"), + failureMessage == s""" + |Tests failed: + | + |foo HelloTests - failure: AssertionError: Expected , actual . + | + |""".stripMargin, + doneMessage == s""" + |Tests: 2, Passed: 1, Failed: 1, Skipped: 0 + | + |Full report is available at $xmlReport + |""".stripMargin, + testResults.length == 2, + testResults.count(result => + result.status == Status.Failure.name() && result.exceptionTrace.getOrElse(Seq.empty).isEmpty + ) == 0 ) } } diff --git a/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSLinkTests.scala b/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSLinkTests.scala index 928a3fcb49e..c0ad36ac29d 100644 --- a/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSLinkTests.scala +++ b/kotlinlib/test/src/mill/kotlinlib/js/KotlinJSLinkTests.scala @@ -4,8 +4,6 @@ import mill.testkit.{TestBaseModule, UnitTester} import mill.{Cross, T} import utest.{TestSuite, Tests, assert, test} -import scala.util.Random - object KotlinJSLinkTests extends TestSuite { private val kotlinVersion = "1.9.25" diff --git a/main/util/src/mill/util/Jvm.scala b/main/util/src/mill/util/Jvm.scala index aaff3514375..c23aebc1839 100644 --- a/main/util/src/mill/util/Jvm.scala +++ b/main/util/src/mill/util/Jvm.scala @@ -204,17 +204,19 @@ object Jvm extends CoursierSupport { if (backgroundOutputs.nonEmpty) spawnSubprocessWithBackgroundOutputs(args, envArgs, workingDir, backgroundOutputs) else - runSubprocess(args, envArgs, workingDir) + runSubprocess(args, envArgs, workingDir).getOrThrow } /** * Runs a generic subprocess and waits for it to terminate. + * + * @return Result with exit code. */ def runSubprocess( commandArgs: Seq[String], envArgs: Map[String, String], workingDir: os.Path - ): Unit = { + ): Result[Int] = { val process = spawnSubprocessWithBackgroundOutputs( commandArgs, envArgs, @@ -239,8 +241,11 @@ object Jvm extends CoursierSupport { } finally { Runtime.getRuntime().removeShutdownHook(shutdownHook) } - if (process.exitCode() == 0) () - else throw new Exception("Interactive Subprocess Failed (exit code " + process.exitCode() + ")") + if (process.exitCode() == 0) Result.Success(process.exitCode()) + else Result.Failure( + "Interactive Subprocess Failed (exit code " + process.exitCode() + ")", + Some(process.exitCode()) + ) } /** diff --git a/scalalib/src/mill/scalalib/JavaModule.scala b/scalalib/src/mill/scalalib/JavaModule.scala index e7da0fb7760..fa831d36320 100644 --- a/scalalib/src/mill/scalalib/JavaModule.scala +++ b/scalalib/src/mill/scalalib/JavaModule.scala @@ -759,7 +759,7 @@ trait JavaModule commandArgs = Seq(Jvm.jdkTool("javadoc")) ++ cmdArgs, envArgs = Map(), workingDir = T.dest - ) + ).getOrThrow } Jvm.createJar(Agg(javadocDir))(outDir) diff --git a/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala b/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala index a5534b4340d..2b4d1d46edd 100644 --- a/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala +++ b/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala @@ -54,7 +54,7 @@ object SonatypeHelpers { val fileName = file.toString val command = "gpg" +: args :+ fileName - Jvm.runSubprocess(command, env, workspace) + Jvm.runSubprocess(command, env, workspace).getOrThrow os.Path(fileName + ".asc") } diff --git a/scalalib/test/src/mill/scalalib/AssemblyTestUtils.scala b/scalalib/test/src/mill/scalalib/AssemblyTestUtils.scala index c6f1ecd16f2..dbcd9e8b9c3 100644 --- a/scalalib/test/src/mill/scalalib/AssemblyTestUtils.scala +++ b/scalalib/test/src/mill/scalalib/AssemblyTestUtils.scala @@ -54,13 +54,13 @@ trait AssemblyTestUtils { commandArgs = Seq(Jvm.javaExe, "-jar", file.toString(), "--text", "tutu"), envArgs = Map.empty[String, String], workingDir = wd - ) + ).getOrThrow if (checkExe) { Jvm.runSubprocess( commandArgs = Seq(file.toString(), "--text", "tutu"), envArgs = Map.empty[String, String], workingDir = wd - ) + ).getOrThrow } } } diff --git a/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala b/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala index 44c18789178..e433cfacf83 100644 --- a/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala +++ b/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala @@ -278,7 +278,8 @@ trait ScalaNativeModule extends ScalaModule { outer => commandArgs = Vector(nativeLink().toString) ++ args().value, envArgs = forkEnv(), workingDir = forkWorkingDir() - ) + ).getOrThrow + () } @internal