diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index f1a6f7a780..2c21044957 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -3,7 +3,8 @@ package scala.build.internal import coursier.jvm.Execve import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv import org.scalajs.jsenv.nodejs.NodeJSEnv -import org.scalajs.jsenv.{Input, RunConfig} +import org.scalajs.jsenv.{Input, JSEnv, RunConfig} +import org.scalajs.testing.adapter.TestAdapter as ScalaJsTestAdapter import sbt.testing.{Framework, Status} import java.io.File @@ -11,9 +12,12 @@ import java.nio.file.{Files, Path, Paths} import scala.build.EitherCps.{either, value} import scala.build.Logger -import scala.build.errors._ +import scala.build.Ops.EitherSeqOps +import scala.build.errors.* import scala.build.internals.EnvVar +import scala.build.testrunner.FrameworkUtils.* import scala.build.testrunner.{AsmTestRunner, TestRunner} +import scala.scalanative.testinterface.adapter.TestAdapter as ScalaNativeTestAdapter import scala.util.{Failure, Properties, Success} object Runner { @@ -238,22 +242,20 @@ object Runner { sourceMap: Boolean = false, esModule: Boolean = false ): Either[BuildException, Process] = either { - - import logger.{log, debug} - - val nodePath = value(findInPath("node").map(_.toString).toRight(NodeNotFoundError())) - - if (!jsDom && allowExecve && Execve.available()) { - + val nodePath: String = + value(findInPath("node") + .map(_.toString) + .toRight(NodeNotFoundError())) + if !jsDom && allowExecve && Execve.available() then { val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args - log( + logger.log( s"Running ${command.mkString(" ")}", " Running" + System.lineSeparator() + command.iterator.map(_ + System.lineSeparator()).mkString ) - debug("execve available") + logger.debug("execve available") Execve.execve( command.head, "node" +: command.tail.toArray, @@ -262,40 +264,36 @@ object Runner { sys.error("should not happen") } else { - val nodeArgs = // Scala.js runs apps by piping JS to node. // If we need to pass arguments, we must first make the piped input explicit // with "-", and we pass the user's arguments after that. - if (args.isEmpty) Nil - else "-" :: args.toList + if args.isEmpty then Nil else "-" :: args.toList val envJs = - if (jsDom) + if jsDom then new JSDOMNodeJSEnv( JSDOMNodeJSEnv.Config() .withExecutable(nodePath) .withArgs(nodeArgs) .withEnv(Map.empty) ) - else new NodeJSEnv( - NodeJSEnv.Config() - .withExecutable(nodePath) - .withArgs(nodeArgs) - .withEnv(Map.empty) - .withSourceMap(sourceMap) - ) + else + new NodeJSEnv( + NodeJSEnv.Config() + .withExecutable(nodePath) + .withArgs(nodeArgs) + .withEnv(Map.empty) + .withSourceMap(sourceMap) + ) - val inputs = Seq( - if (esModule) Input.ESModule(entrypoint.toPath) - else Input.Script(entrypoint.toPath) - ) + val inputs = + Seq(if esModule then Input.ESModule(entrypoint.toPath) else Input.Script(entrypoint.toPath)) val config = RunConfig().withLogger(logger.scalaJsLogger) val processJs = envJs.start(inputs, config) processJs.future.value.foreach { - case Failure(t) => - throw new Exception(t) + case Failure(t) => throw new Exception(t) case Success(_) => } @@ -346,32 +344,30 @@ object Runner { private def runTests( classPath: Seq[Path], - framework: Framework, + frameworks: Seq[Framework], requireTests: Boolean, args: Seq[String], parentInspector: AsmTestRunner.ParentInspector - ): Either[NoTestsRun, Boolean] = { - - val taskDefs = - AsmTestRunner.taskDefs( - classPath, - keepJars = false, - framework.fingerprints().toIndexedSeq, - parentInspector - ).toArray - - val runner = framework.runner(args.toArray, Array(), null) - val initialTasks = runner.tasks(taskDefs) - val events = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out) - - val doneMsg = runner.done() - if (doneMsg.nonEmpty) - System.out.println(doneMsg) - - if (requireTests && events.isEmpty) - Left(new NoTestsRun) - else - Right { + ): Either[NoTestsRun, Boolean] = frameworks + .flatMap { framework => + val taskDefs = + AsmTestRunner.taskDefs( + classPath, + keepJars = false, + framework.fingerprints().toIndexedSeq, + parentInspector + ).toArray + + val runner = framework.runner(args.toArray, Array(), null) + val initialTasks = runner.tasks(taskDefs) + val events = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out) + + val doneMsg = runner.done() + if doneMsg.nonEmpty then System.out.println(doneMsg) + events + } match { + case events if requireTests && events.isEmpty => Left(new NoTestsRun) + case events => Right { !events.exists { ev => ev.status == Status.Error || ev.status == Status.Failure || @@ -380,22 +376,30 @@ object Runner { } } - def frameworkName( + def frameworkNames( classPath: Seq[Path], - parentInspector: AsmTestRunner.ParentInspector - ): Either[NoTestFrameworkFoundError, String] = { - val fwOpt = AsmTestRunner.findFrameworkService(classPath) - .orElse { - AsmTestRunner.findFramework( - classPath, - TestRunner.commonTestFrameworks, - parentInspector - ) - } - fwOpt match { - case Some(fw) => Right(fw.replace('/', '.').replace('\\', '.')) - case None => Left(new NoTestFrameworkFoundError) - } + parentInspector: AsmTestRunner.ParentInspector, + logger: Logger + ): Either[NoTestFrameworkFoundError, Seq[String]] = { + logger.debug("Looking for test framework services on the classpath...") + val foundFrameworkServices = + AsmTestRunner.findFrameworkServices(classPath) + .map(_.replace('/', '.').replace('\\', '.')) + logger.debug(s"Found ${foundFrameworkServices.length} test framework services.") + if foundFrameworkServices.nonEmpty then + logger.debug(s" - ${foundFrameworkServices.mkString("\n - ")}") + logger.debug("Looking for more test frameworks on the classpath...") + val foundFrameworks = + AsmTestRunner.findFrameworks(classPath, TestRunner.commonTestFrameworks, parentInspector) + .map(_.replace('/', '.').replace('\\', '.')) + logger.debug(s"Found ${foundFrameworks.length} additional test frameworks") + if foundFrameworks.nonEmpty then + logger.debug(s" - ${foundFrameworks.mkString("\n - ")}") + val frameworks: Seq[String] = foundFrameworkServices ++ foundFrameworks + logger.log(s"Found ${frameworks.length} test frameworks in total") + if frameworks.nonEmpty then + logger.debug(s" - ${frameworks.mkString("\n - ")}") + if frameworks.nonEmpty then Right(frameworks) else Left(new NoTestFrameworkFoundError) } def testJs( @@ -410,57 +414,72 @@ object Runner { ): Either[TestError, Int] = either { import org.scalajs.jsenv.Input import org.scalajs.jsenv.nodejs.NodeJSEnv - import org.scalajs.testing.adapter.TestAdapter + logger.debug("Preparing to run tests with Scala.js...") + logger.debug(s"Scala.js tests class path: $classPath") val nodePath = findInPath("node").fold("node")(_.toString) - val jsEnv = - if (jsDom) + logger.debug(s"Node found at $nodePath") + val jsEnv: JSEnv = + if jsDom then { + logger.log("Loading JS environment with JS DOM...") new JSDOMNodeJSEnv( JSDOMNodeJSEnv.Config() .withExecutable(nodePath) .withArgs(Nil) .withEnv(Map.empty) ) - else new NodeJSEnv( - NodeJSEnv.Config() - .withExecutable(nodePath) - .withArgs(Nil) - .withEnv(Map.empty) - .withSourceMap(NodeJSEnv.SourceMap.Disable) - ) - val adapterConfig = TestAdapter.Config().withLogger(logger.scalaJsLogger) - val inputs = Seq( - if (esModule) Input.ESModule(entrypoint.toPath) - else Input.Script(entrypoint.toPath) - ) - var adapter: TestAdapter = null + } + else { + logger.log("Loading JS environment with Node...") + new NodeJSEnv( + NodeJSEnv.Config() + .withExecutable(nodePath) + .withArgs(Nil) + .withEnv(Map.empty) + .withSourceMap(NodeJSEnv.SourceMap.Disable) + ) + } + val adapterConfig = ScalaJsTestAdapter.Config().withLogger(logger.scalaJsLogger) + val inputs = + Seq(if esModule then Input.ESModule(entrypoint.toPath) else Input.Script(entrypoint.toPath)) + var adapter: ScalaJsTestAdapter = null logger.debug(s"JS tests class path: $classPath") val parentInspector = new AsmTestRunner.ParentInspector(classPath) - val frameworkName0 = testFrameworkOpt match { - case Some(fw) => fw - case None => value(frameworkName(classPath, parentInspector)) + val foundFrameworkNames: List[String] = testFrameworkOpt match { + case some @ Some(_) => some.toList + case None => value(frameworkNames(classPath, parentInspector, logger)).toList } val res = try { - adapter = new TestAdapter(jsEnv, inputs, adapterConfig) - - val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten + adapter = new ScalaJsTestAdapter(jsEnv, inputs, adapterConfig) + + val loadedFrameworks = + adapter + .loadFrameworks(foundFrameworkNames.map(List(_))) + .flatten + .distinctBy(_.name()) + + val finalTestFrameworks = + loadedFrameworks + .filter( + !_.name().toLowerCase.contains("junit") || + !loadedFrameworks.exists(_.name().toLowerCase.contains("munit")) + ) + if finalTestFrameworks.nonEmpty then + logger.log( + s"""Final list of test frameworks found: + | - ${finalTestFrameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) - if (frameworks.isEmpty) - Left(new NoFrameworkFoundByBridgeError) - else if (frameworks.length > 1) - Left(new TooManyFrameworksFoundByBridgeError) - else { - val framework = frameworks.head - runTests(classPath, framework, requireTests, args, parentInspector) - } + if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError) + else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector) } - finally if (adapter != null) adapter.close() + finally if adapter != null then adapter.close() - if (value(res)) 0 - else 1 + if value(res) then 0 else 1 } def testNative( @@ -471,42 +490,61 @@ object Runner { args: Seq[String], logger: Logger ): Either[TestError, Int] = either { - - import scala.scalanative.testinterface.adapter.TestAdapter - + logger.debug("Preparing to run tests with Scala Native...") logger.debug(s"Native tests class path: $classPath") val parentInspector = new AsmTestRunner.ParentInspector(classPath) - val frameworkName0 = frameworkNameOpt match { - case Some(fw) => fw - case None => value(frameworkName(classPath, parentInspector)) + val foundFrameworkNames: List[String] = frameworkNameOpt match { + case Some(fw) => List(fw) + case None => value(frameworkNames(classPath, parentInspector, logger)).toList } - val config = TestAdapter.Config() + val config = ScalaNativeTestAdapter.Config() .withBinaryFile(launcher) - .withEnvVars(sys.env.toMap) + .withEnvVars(sys.env) .withLogger(logger.scalaNativeTestLogger) - var adapter: TestAdapter = null + var adapter: ScalaNativeTestAdapter = null val res = try { - adapter = new TestAdapter(config) + adapter = new ScalaNativeTestAdapter(config) + + val loadedFrameworks = + adapter + .loadFrameworks(foundFrameworkNames.map(List(_))) + .flatten + .distinctBy(_.name()) + + val finalTestFrameworks = + loadedFrameworks + // .filter( + // _.name() != "Scala Native JUnit test framework" || + // !loadedFrameworks.exists(_.name() == "munit") + // ) + // TODO: add support for JUnit and then only hardcode filtering it out when passed with munit + // https://github.com/VirtusLab/scala-cli/issues/3627 + .filter(_.name() != "Scala Native JUnit test framework") + if finalTestFrameworks.nonEmpty then + logger.log( + s"""Final list of test frameworks found: + | - ${finalTestFrameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) - val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten + val skippedFrameworks = loadedFrameworks.diff(finalTestFrameworks) + if skippedFrameworks.nonEmpty then + logger.log( + s"""The following test frameworks have been filtered out: + | - ${skippedFrameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) - if (frameworks.isEmpty) - Left(new NoFrameworkFoundByBridgeError) - else if (frameworks.length > 1) - Left(new TooManyFrameworksFoundByBridgeError) - else { - val framework = frameworks.head - runTests(classPath, framework, requireTests, args, parentInspector) - } + if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError) + else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector) } - finally if (adapter != null) adapter.close() + finally if adapter != null then adapter.close() - if (value(res)) 0 - else 1 + if value(res) then 0 else 1 } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala index 3979546dfb..823f0a0850 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala @@ -248,7 +248,7 @@ object Test extends ScalaCommand[TestOptions] { val testOnly = build.options.testOptions.testOnly val extraArgs = - (if (requireTests) Seq("--require-tests") else Nil) ++ + (if requireTests then Seq("--require-tests") else Nil) ++ build.options.internal.verbosity.map(v => s"--verbosity=$v") ++ testFrameworkOpt0.map(fw => s"--test-framework=$fw").toSeq ++ testOnly.map(to => s"--test-only=$to").toSeq ++ @@ -266,16 +266,15 @@ object Test extends ScalaCommand[TestOptions] { } } - def findTestFramework(classPath: Seq[Path], logger: Logger): Option[String] = { + private def findTestFramework(classPath: Seq[Path], logger: Logger): Option[String] = { val classPath0 = classPath.map(_.toString) // https://github.com/VirtusLab/scala-cli/issues/426 - if ( - classPath0.exists(_.contains("zio-test")) && !classPath0.exists(_.contains("zio-test-sbt")) - ) { + if classPath0.exists(_.contains("zio-test")) && !classPath0.exists(_.contains("zio-test-sbt")) + then { val parentInspector = new AsmTestRunner.ParentInspector(classPath) - Runner.frameworkName(classPath, parentInspector) match { - case Right(f) => Some(f) + Runner.frameworkNames(classPath, parentInspector, logger) match { + case Right(f) => f.headOption case Left(_) => logger.message( s"zio-test found in the class path, zio-test-sbt should be added to run zio tests with $fullRunnerName." @@ -283,8 +282,7 @@ object Test extends ScalaCommand[TestOptions] { None } } - else - None + else None } } diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala index f1c0054413..77fc6d46a4 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala @@ -13,7 +13,6 @@ import java.nio.file.Path import scala.build.errors.BuildException import scala.build.info.{BuildInfo, ScopedBuildInfo} import scala.build.internal.Constants -import scala.build.internal.Runner.frameworkName import scala.build.options.{BuildOptions, Scope} import scala.build.testrunner.AsmTestRunner import scala.build.{Logger, Positioned, Sources} diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/MavenProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/MavenProjectDescriptor.scala index 866782b8f9..0c4be4b178 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/MavenProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/MavenProjectDescriptor.scala @@ -10,7 +10,7 @@ import java.nio.file.Path import scala.build.errors.BuildException import scala.build.internal.Constants -import scala.build.internal.Runner.frameworkName +import scala.build.internal.Runner.frameworkNames import scala.build.options.{BuildOptions, Platform, Scope, ShadowingSeq} import scala.build.testrunner.AsmTestRunner import scala.build.{Logger, Positioned, Sources} diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala index b731e86d64..5c2c4e3864 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala @@ -9,7 +9,7 @@ import java.nio.file.Path import scala.build.errors.BuildException import scala.build.internal.Constants -import scala.build.internal.Runner.frameworkName +import scala.build.internal.Runner.frameworkNames import scala.build.options.{BuildOptions, Platform, ScalaJsOptions, ScalaNativeOptions, Scope} import scala.build.testrunner.AsmTestRunner import scala.build.{Logger, Sources} @@ -149,7 +149,8 @@ final case class MillProjectDescriptor( } val parentInspector = new AsmTestRunner.ParentInspector(testClassPath) val frameworkName0 = options.testOptions.frameworkOpt.orElse { - frameworkName(testClassPath, parentInspector).toOption + frameworkNames(testClassPath, parentInspector, logger).toOption + .flatMap(_.headOption) // TODO: handle multiple frameworks here } val testFrameworkDecls = frameworkName0 match { diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala index 64b36f00a5..886ae9974d 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala @@ -10,7 +10,7 @@ import java.nio.file.Path import scala.build.errors.BuildException import scala.build.internal.Constants -import scala.build.internal.Runner.frameworkName +import scala.build.internal.Runner.frameworkNames import scala.build.options.{ BuildOptions, Platform, @@ -262,7 +262,8 @@ final case class SbtProjectDescriptor( val parentInspector = new AsmTestRunner.ParentInspector(testClassPath) val frameworkName0 = options.testOptions.frameworkOpt.orElse { - frameworkName(testClassPath, parentInspector).toOption + frameworkNames(testClassPath, parentInspector, logger).toOption + .flatMap(_.headOption) // TODO: handle multiple frameworks here } val testFrameworkSettings = frameworkName0 match { diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala index 75a165484f..dcf9108f45 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala @@ -9,6 +9,7 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr _: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions private val utestVersion = "0.8.3" + private val zioTestVersion = "2.1.17" def successfulTestInputs(directivesString: String = s"//> using dep org.scalameta::munit::$munitVersion"): TestInputs = TestInputs( @@ -810,4 +811,143 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr expect(res.out.text().contains(expectedMessage)) } } + + test(s"zio-test warning when zio-test-sbt was not passed") { + TestUtil.retryOnCi() { + val expectedMessage = "Hello from zio" + TestInputs(os.rel / "Zio.test.scala" -> + s"""//> using test.dep dev.zio::zio-test::$zioTestVersion + |import zio._ + |import zio.test._ + | + |object SimpleSpec extends ZIOSpecDefault { + | override def spec: Spec[TestEnvironment with Scope, Any] = + | suite("SimpleSpec")( + | test("print hello and assert true") { + | for { + | _ <- Console.printLine("$expectedMessage") + | } yield assertTrue(true) + | } + | ) + |} + |""".stripMargin).fromRoot { root => + val r = os.proc(TestUtil.cli, "test", ".", extraOptions) + .call(cwd = root, check = false, stderr = os.Pipe) + expect(r.exitCode == 1) + val expectedWarning = + "zio-test found in the class path, zio-test-sbt should be added to run zio tests" + expect(r.err.trim().contains(expectedWarning)) + } + } + } + + for { + platformOptions <- Seq( + Nil, // JVM + Seq("--native"), + Seq("--js") + ) + platformDescription = platformOptions.headOption.map(o => s" ($o)").getOrElse(" (JVM)") + } { + test(s"zio-test$platformDescription") { + TestUtil.retryOnCi() { + val expectedMessage = "Hello from zio" + TestInputs(os.rel / "Zio.test.scala" -> + s"""//> using test.dep dev.zio::zio-test::$zioTestVersion + |//> using test.dep dev.zio::zio-test-sbt::$zioTestVersion + |import zio._ + |import zio.test._ + | + |object SimpleSpec extends ZIOSpecDefault { + | override def spec: Spec[TestEnvironment with Scope, Any] = + | suite("SimpleSpec")( + | test("print hello and assert true") { + | for { + | _ <- Console.printLine("$expectedMessage") + | } yield assertTrue(true) + | } + | ) + |} + |""".stripMargin).fromRoot { root => + val r = os.proc(TestUtil.cli, "test", ".", extraOptions, platformOptions).call(cwd = root) + val output = r.out.trim() + expect(output.contains(expectedMessage)) + expect(countSubStrings(output, expectedMessage) == 1) + } + } + } + + test(s"multiple test frameworks$platformDescription") { + TestUtil.retryOnCi() { + val expectedMessages @ Seq(scalatestMessage, munitMessage, utestMessage, zioMessage) = + Seq("Hello from ScalaTest", "Hello from Munit", "Hello from utest", "Hello from zio") + TestInputs( + os.rel / "project.scala" -> + s"""//> using test.dep org.scalatest::scalatest::3.2.19 + |//> using test.dep org.scalameta::munit::$munitVersion + |//> using dep com.lihaoyi::utest::$utestVersion + |//> using test.dep dev.zio::zio-test::$zioTestVersion + |//> using test.dep dev.zio::zio-test-sbt::$zioTestVersion + |""".stripMargin, + os.rel / "scalatest.test.scala" -> + s"""import org.scalatest.flatspec.AnyFlatSpec + | + |class ScalaTestSpec extends AnyFlatSpec { + | "example" should "work" in { + | assertResult(1)(1) + | println("$scalatestMessage") + | } + |} + |""".stripMargin, + os.rel / "munit.test.scala" -> + s"""import munit.FunSuite + | + |class Munit extends FunSuite { + | test("foo") { + | assert(2 + 2 == 4) + | println("$munitMessage") + | } + |} + |""".stripMargin, + os.rel / "utest.test.scala" -> + s"""import utest._ + | + |object MyTests extends TestSuite { + | val tests = Tests { + | test("foo") { + | assert(2 + 2 == 4) + | println("$utestMessage") + | } + | } + |}""".stripMargin, + os.rel / "Zio.test.scala" -> + s"""import zio._ + |import zio.test._ + | + |object SimpleSpec extends ZIOSpecDefault { + | override def spec: Spec[TestEnvironment with Scope, Any] = + | suite("SimpleSpec")( + | test("print hello and assert true") { + | for { + | _ <- Console.printLine("$zioMessage") + | } yield assertTrue(true) + | } + | ) + |} + |""".stripMargin + ).fromRoot { root => + val r = + os.proc(TestUtil.cli, "test", extraOptions, ".", platformOptions, "-v", "-v").call(cwd = + root + ) + val output = r.out.trim() + expect(output.nonEmpty) + expectedMessages.foreach { expectedMessage => + expect(output.contains(expectedMessage)) + expect(countSubStrings(output, expectedMessage) == 1) + } + } + } + } + } } diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala index 3a9f507264..f4e223ff41 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala @@ -1,7 +1,7 @@ package scala.build.testrunner import org.objectweb.asm -import sbt.testing._ +import sbt.testing.{Logger => _, _} import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} import java.nio.charset.StandardCharsets @@ -20,7 +20,11 @@ object AsmTestRunner { Option(cache.get(className)) match { case Some(value) => value case None => - val byteCodeOpt = findInClassPath(classPath, className + ".class") + val byteCodeOpt = + findInClassPath(classPath, className + ".class") + .take(1) + .toList + .headOption val parents = byteCodeOpt match { case None => Nil case Some(b) => @@ -89,7 +93,7 @@ object AsmTestRunner { // cls.getDeclaredMethods.exists(_.isAnnotationPresent(annotationCls)) || // cls.getMethods.exists(m => m.isAnnotationPresent(annotationCls) && Modifier.isPublic(m.getModifiers())) // ) - ??? + ??? // TODO: this is necessary to support JUnit, check https://github.com/VirtusLab/scala-cli/issues/3627 } } @@ -186,44 +190,31 @@ object AsmTestRunner { } else None - def findInClassPath(classPath: Seq[Path], name: String): Option[Array[Byte]] = + def findInClassPath(classPath: Seq[Path], name: String): Iterator[Array[Byte]] = classPath .iterator .flatMap(findInClassPath(_, name).iterator) - .take(1) - .toList - .headOption - - def findFrameworkService(classPath: Seq[Path]): Option[String] = - findInClassPath(classPath, "META-INF/services/sbt.testing.Framework").map { b => - new String(b, StandardCharsets.UTF_8) - } - def findFramework( - classPath: Seq[Path], - preferredClasses: Seq[String] - ): Option[String] = { - val parentInspector = new ParentInspector(classPath) - findFramework(classPath, preferredClasses, parentInspector) - } + def findFrameworkServices(classPath: Seq[Path]): Seq[String] = + findInClassPath(classPath, "META-INF/services/sbt.testing.Framework") + .map(b => new String(b, StandardCharsets.UTF_8)) + .toSeq - def findFramework( + def findFrameworks( classPath: Seq[Path], preferredClasses: Seq[String], parentInspector: ParentInspector - ): Option[String] = { + ): List[String] = { val preferredClassesByteCode = preferredClasses - .iterator .map(_.replace('.', '/')) .flatMap { name => findInClassPath(classPath, name + ".class") - .iterator .map { b => def openStream() = new ByteArrayInputStream(b) - (name, openStream _) + (name, () => openStream()) } } - (preferredClassesByteCode ++ listClassesByteCode(classPath, true)) + (preferredClassesByteCode.iterator ++ listClassesByteCode(classPath, true)) .flatMap { case (moduleInfo, _) if moduleInfo.contains("module-info") => Iterator.empty case (name, is) => @@ -241,9 +232,8 @@ object AsmTestRunner { else Iterator.empty } - .take(1) + .take(math.max(preferredClassesByteCode.length, 1)) .toList - .headOption } private class TestClassChecker extends asm.ClassVisitor(asm.Opcodes.ASM9) { @@ -252,18 +242,11 @@ object AsmTestRunner { private var isInterfaceOpt = Option.empty[Boolean] private var isAbstractOpt = Option.empty[Boolean] private var implements0 = List.empty[String] - def canBeTestSuite: Boolean = { - val isModule = nameOpt.exists(_.endsWith("$")) - !isAbstractOpt.contains(true) && - !isInterfaceOpt.contains(true) && - publicConstructorCount0 <= 1 && - isModule != (publicConstructorCount0 == 1) - } - def name = nameOpt.getOrElse(sys.error("Class not visited")) - def publicConstructorCount = publicConstructorCount0 - def implements = implements0 - def isAbstract = isAbstractOpt.getOrElse(sys.error("Class not visited")) - def isInterface = isInterfaceOpt.getOrElse(sys.error("Class not visited")) + def name: String = nameOpt.getOrElse(sys.error("Class not visited")) + def publicConstructorCount: Int = publicConstructorCount0 + def implements: Seq[String] = implements0 + def isAbstract: Boolean = isAbstractOpt.getOrElse(sys.error("Class not visited")) + def isInterface: Boolean = isInterfaceOpt.getOrElse(sys.error("Class not visited")) override def visit( version: Int, access: Int, @@ -323,8 +306,8 @@ object AsmTestRunner { val parentCache = new ParentInspector(classPath) - val frameworkClassName = findFrameworkService(classPath) - .orElse(findFramework(classPath, TestRunner.commonTestFrameworks, parentCache)) + val frameworkClassName = findFrameworkServices(classPath).headOption // TODO handle multiple + .orElse(findFrameworks(classPath, TestRunner.commonTestFrameworks, parentCache).headOption) .getOrElse(sys.error("No test framework found")) .replace('/', '.') .replace('\\', '.') diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala index c168d9d9cd..7fd757fca2 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala @@ -1,154 +1,14 @@ package scala.build.testrunner -import sbt.testing._ +import sbt.testing.{Logger => _, _} -import java.lang.annotation.Annotation -import java.lang.reflect.Modifier -import java.nio.file.{Files, Path} -import java.util.ServiceLoader import java.util.regex.Pattern import scala.annotation.tailrec -import scala.jdk.CollectionConverters._ +import scala.build.testrunner.FrameworkUtils._ object DynamicTestRunner { - // adapted from https://github.com/com-lihaoyi/mill/blob/ab4d61a50da24fb7fac97c4453dd8a770d8ac62b/scalalib/src/Lib.scala#L156-L172 - private def matchFingerprints( - loader: ClassLoader, - cls: Class[_], - fingerprints: Array[Fingerprint] - ): Option[Fingerprint] = { - val isModule = cls.getName.endsWith("$") - val publicConstructorCount = cls.getConstructors.count(c => Modifier.isPublic(c.getModifiers)) - val noPublicConstructors = publicConstructorCount == 0 - val definitelyNoTests = Modifier.isAbstract(cls.getModifiers) || - cls.isInterface || - publicConstructorCount > 1 || - isModule != noPublicConstructors - if (definitelyNoTests) - None - else - fingerprints.find { - case f: SubclassFingerprint => - f.isModule == isModule && - loader.loadClass(f.superclassName()) - .isAssignableFrom(cls) - - case f: AnnotatedFingerprint => - val annotationCls = loader.loadClass(f.annotationName()) - .asInstanceOf[Class[Annotation]] - f.isModule == isModule && ( - cls.isAnnotationPresent(annotationCls) || - cls.getDeclaredMethods.exists(_.isAnnotationPresent(annotationCls)) || - cls.getMethods.exists { m => - m.isAnnotationPresent(annotationCls) && - Modifier.isPublic(m.getModifiers()) - } - ) - } - } - - def listClasses(classPathEntry: Path, keepJars: Boolean): Iterator[String] = - if (Files.isDirectory(classPathEntry)) { - var stream: java.util.stream.Stream[Path] = null - try { - stream = Files.walk(classPathEntry, Int.MaxValue) - stream - .iterator - .asScala - .filter(_.getFileName.toString.endsWith(".class")) - .map(classPathEntry.relativize(_)) - .map { p => - val count = p.getNameCount - (0 until count).map(p.getName).mkString(".") - } - .map(_.stripSuffix(".class")) - .toVector // fully consume stream before closing it - .iterator - } - finally if (stream != null) stream.close() - } - else if (keepJars && Files.isRegularFile(classPathEntry)) { - import java.util.zip._ - var zf: ZipFile = null - try { - zf = new ZipFile(classPathEntry.toFile) - zf.entries - .asScala - // FIXME Check if these are files too - .filter(_.getName.endsWith(".class")) - .map(ent => ent.getName.stripSuffix(".class").replace("/", ".")) - .toVector // full consume ZipFile before closing it - .iterator - } - finally if (zf != null) zf.close() - } - else Iterator.empty - - def listClasses(classPath: Seq[Path], keepJars: Boolean): Iterator[String] = - classPath.iterator.flatMap(listClasses(_, keepJars)) - - def findFrameworkService(loader: ClassLoader): Option[Framework] = - ServiceLoader.load(classOf[Framework], loader) - .iterator() - .asScala - .take(1) - .toList - .headOption - - def loadFramework( - loader: ClassLoader, - className: String - ): Framework = { - val cls = loader.loadClass(className) - val constructor = cls.getConstructor() - constructor.newInstance().asInstanceOf[Framework] - } - - def findFramework( - classPath: Seq[Path], - loader: ClassLoader, - preferredClasses: Seq[String] - ): Option[Framework] = { - val frameworkCls = classOf[Framework] - (preferredClasses.iterator ++ listClasses(classPath, true)) - .flatMap { name => - val it: Iterator[Class[_]] = - try Iterator(loader.loadClass(name)) - catch { - case _: ClassNotFoundException | _: UnsupportedClassVersionError | _: NoClassDefFoundError | _: IncompatibleClassChangeError => - Iterator.empty - } - it - } - .flatMap { cls => - def isAbstract = Modifier.isAbstract(cls.getModifiers) - def publicConstructorCount = - cls.getConstructors.count { c => - Modifier.isPublic(c.getModifiers) && c.getParameterCount() == 0 - } - val it: Iterator[Class[_]] = - if (frameworkCls.isAssignableFrom(cls) && !isAbstract && publicConstructorCount == 1) - Iterator(cls) - else - Iterator.empty - it - } - .flatMap { cls => - try { - val constructor = cls.getConstructor() - Iterator(constructor.newInstance().asInstanceOf[Framework]) - } - catch { - case _: NoSuchMethodException => Iterator.empty - } - } - .take(1) - .toList - .headOption - } - /** Based on junit-interface [GlobFilter. * compileGlobPattern](https://github.com/sbt/junit-interface/blob/f8c6372ed01ce86f15393b890323d96afbe6d594/src/main/java/com/novocode/junit/GlobFilter.java#L37) * @@ -218,17 +78,23 @@ object DynamicTestRunner { parse(None, Nil, false, 0, None, args.toList) } + val logger = Logger(verbosity) + val classLoader = Thread.currentThread().getContextClassLoader val classPath0 = TestRunner.classPath(classLoader) - val framework = testFrameworkOpt.map(loadFramework(classLoader, _)) - .orElse(findFrameworkService(classLoader)) - .orElse(findFramework(classPath0, classLoader, TestRunner.commonTestFrameworks)) + val frameworks = testFrameworkOpt + .map(loadFramework(classLoader, _)) + .map(Seq(_)) .getOrElse { - if (verbosity >= 2) - sys.error("No test framework found") - else { - System.err.println("No test framework found") - sys.exit(1) + getFrameworksToRun( + frameworkServices = findFrameworkServices(classLoader), + frameworks = findFrameworks(classPath0, classLoader, TestRunner.commonTestFrameworks) + )(logger) match { + case f if f.nonEmpty => f + case _ if verbosity >= 2 => sys.error("No test framework found") + case _ => + System.err.println("No test framework found") + sys.exit(1) } } def classes = { @@ -237,41 +103,55 @@ object DynamicTestRunner { } val out = System.out - val fingerprints = framework.fingerprints() - val runner = framework.runner(args0.toArray, Array(), classLoader) - def clsFingerprints = classes.flatMap { cls => - matchFingerprints(classLoader, cls, fingerprints) - .map((cls, _)) - .iterator - } - val taskDefs = clsFingerprints - .filter { - case (cls, _) => - testOnly.forall(pattern => - globPattern(pattern).matcher(cls.getName.stripSuffix("$")).matches() - ) - } - .map { - case (cls, fp) => - new TaskDef(cls.getName.stripSuffix("$"), fp, false, Array(new SuiteSelector)) - } - .toVector - val initialTasks = runner.tasks(taskDefs.toArray) - val events = TestRunner.runTasks(initialTasks, out) - val failed = events.exists { ev => - ev.status == Status.Error || - ev.status == Status.Failure || - ev.status == Status.Canceled - } - val doneMsg = runner.done() - if (doneMsg.nonEmpty) - out.println(doneMsg) - if (requireTests && events.isEmpty) { - System.err.println("Error: no tests were run.") - sys.exit(1) - } - if (failed) - sys.exit(1) + val exitCodes = + frameworks + .map { framework => + logger.log(s"Running test framework: ${framework.name}") + val fingerprints = framework.fingerprints() + val runner = framework.runner(args0.toArray, Array(), classLoader) + + def clsFingerprints = classes.flatMap { cls => + matchFingerprints(classLoader, cls, fingerprints) + .map((cls, _)) + .iterator + } + + val taskDefs = clsFingerprints + .filter { + case (cls, _) => + testOnly.forall(pattern => + globPattern(pattern).matcher(cls.getName.stripSuffix("$")).matches() + ) + } + .map { + case (cls, fp) => + new TaskDef(cls.getName.stripSuffix("$"), fp, false, Array(new SuiteSelector)) + } + .toVector + val initialTasks = runner.tasks(taskDefs.toArray) + val events = TestRunner.runTasks(initialTasks, out) + val failed = events.exists { ev => + ev.status == Status.Error || + ev.status == Status.Failure || + ev.status == Status.Canceled + } + val doneMsg = runner.done() + if (doneMsg.nonEmpty) out.println(doneMsg) + if (requireTests && events.isEmpty) { + logger.error(s"Error: no tests were run for ${framework.name()}.") + 1 + } + else if (failed) { + logger.error(s"Error: ${framework.name()} tests failed.") + 1 + } + else { + logger.log(s"${framework.name()} tests ran successfully.") + 0 + } + } + if (exitCodes.contains(1)) sys.exit(1) + else sys.exit(0) } } diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala new file mode 100644 index 0000000000..90d894d452 --- /dev/null +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala @@ -0,0 +1,223 @@ +package scala.build.testrunner + +import sbt.testing.{AnnotatedFingerprint, Fingerprint, Framework, SubclassFingerprint} + +import java.lang.annotation.Annotation +import java.lang.reflect.Modifier +import java.nio.file.{Files, Path} +import java.util.ServiceLoader + +import scala.jdk.CollectionConverters._ + +object FrameworkUtils { + // needed for Scala 2.12 + def distinctBy[A, B](seq: Seq[A])(f: A => B): Seq[A] = { + @annotation.tailrec + def loop(remaining: Seq[A], seen: Set[B], acc: Vector[A]): Vector[A] = + if (remaining.isEmpty) acc + else { + val head = remaining.head + val tail = remaining.tail + val key = f(head) + if (seen(key)) loop(tail, seen, acc) + else loop(tail, seen + key, acc :+ head) + } + + loop(seq, Set.empty, Vector.empty) + } + + implicit class TestFrameworkOps(val framework: Framework) { + def description: String = + s"${framework.name()} (${Option(framework.getClass.getCanonicalName).getOrElse(framework.toString)})" + } + + def getFrameworksToRun(allFrameworks: Seq[Framework])(logger: Logger): Seq[Framework] = { + val distinctFrameworks = distinctBy(allFrameworks)(_.name()) + if (distinctFrameworks.nonEmpty) + logger.debug( + s"""Distinct test frameworks found (by framework name): + | - ${distinctFrameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) + + val finalFrameworks = + distinctFrameworks + .filter(f1 => + !distinctFrameworks + .filter(_ != f1) + .exists(f2 => + f1.getClass.isAssignableFrom(f2.getClass) + ) + ) + if (finalFrameworks.nonEmpty) + logger.log( + s"""Final list of test frameworks found: + | - ${finalFrameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) + + val skippedInheritedFrameworks = distinctFrameworks.diff(finalFrameworks) + if (skippedInheritedFrameworks.nonEmpty) + logger.log( + s"""The following test frameworks have been filtered out, as they're being inherited from by others: + | - ${skippedInheritedFrameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) + + finalFrameworks + } + + def getFrameworksToRun( + frameworkServices: Seq[Framework], + frameworks: Seq[Framework] + )(logger: Logger): Seq[Framework] = { + if (frameworkServices.nonEmpty) + logger.debug( + s"""Found test framework services: + | - ${frameworkServices.map(_.description).mkString("\n - ")} + |""".stripMargin + ) + if (frameworks.nonEmpty) + logger.debug( + s"""Found test frameworks: + | - ${frameworks.map(_.description).mkString("\n - ")} + |""".stripMargin + ) + + getFrameworksToRun(allFrameworks = frameworkServices ++ frameworks)(logger) + } + + def listClasses(classPath: Seq[Path], keepJars: Boolean): Iterator[String] = + classPath.iterator.flatMap(listClasses(_, keepJars)) + + def listClasses(classPathEntry: Path, keepJars: Boolean): Iterator[String] = + if (Files.isDirectory(classPathEntry)) { + var stream: java.util.stream.Stream[Path] = null + try { + stream = Files.walk(classPathEntry, Int.MaxValue) + stream + .iterator + .asScala + .filter(_.getFileName.toString.endsWith(".class")) + .map(classPathEntry.relativize) + .map { p => + val count = p.getNameCount + (0 until count).map(p.getName).mkString(".") + } + .map(_.stripSuffix(".class")) + .toVector // fully consume stream before closing it + .iterator + } + finally if (stream != null) stream.close() + } + else if (keepJars && Files.isRegularFile(classPathEntry)) { + import java.util.zip._ + var zf: ZipFile = null + try { + zf = new ZipFile(classPathEntry.toFile) + zf.entries + .asScala + // FIXME Check if these are files too + .filter(_.getName.endsWith(".class")) + .map(ent => ent.getName.stripSuffix(".class").replace("/", ".")) + .toVector // full consume ZipFile before closing it + .iterator + } + finally if (zf != null) zf.close() + } + else Iterator.empty + + def findFrameworkServices(loader: ClassLoader): Seq[Framework] = + ServiceLoader.load(classOf[Framework], loader) + .iterator() + .asScala + .toSeq + + def loadFramework( + loader: ClassLoader, + className: String + ): Framework = { + val cls = loader.loadClass(className) + val constructor = cls.getConstructor() + constructor.newInstance().asInstanceOf[Framework] + } + + def findFrameworks( + classPath: Seq[Path], + loader: ClassLoader, + preferredClasses: Seq[String] + ): Seq[Framework] = { + val frameworkCls = classOf[Framework] + (preferredClasses.iterator ++ listClasses(classPath, true)) + .flatMap { name => + val it: Iterator[Class[_]] = + try Iterator(loader.loadClass(name)) + catch { + case _: ClassNotFoundException | _: UnsupportedClassVersionError | _: NoClassDefFoundError | _: IncompatibleClassChangeError => + Iterator.empty + } + it + } + .flatMap { cls => + def isAbstract = Modifier.isAbstract(cls.getModifiers) + + def publicConstructorCount = + cls.getConstructors.count { c => + Modifier.isPublic(c.getModifiers) && c.getParameterCount == 0 + } + + val it: Iterator[Class[_]] = + if (frameworkCls.isAssignableFrom(cls) && !isAbstract && publicConstructorCount == 1) + Iterator(cls) + else + Iterator.empty + it + } + .flatMap { cls => + try { + val constructor = cls.getConstructor() + Iterator(constructor.newInstance().asInstanceOf[Framework]) + } + catch { + case _: NoSuchMethodException => Iterator.empty + } + } + .toSeq + } + + // adapted from https://github.com/com-lihaoyi/mill/blob/ab4d61a50da24fb7fac97c4453dd8a770d8ac62b/scalalib/src/Lib.scala#L156-L172 + def matchFingerprints( + loader: ClassLoader, + cls: Class[_], + fingerprints: Array[Fingerprint] + ): Option[Fingerprint] = { + val isModule = cls.getName.endsWith("$") + val publicConstructorCount = cls.getConstructors.count(c => Modifier.isPublic(c.getModifiers)) + val noPublicConstructors = publicConstructorCount == 0 + val definitelyNoTests = Modifier.isAbstract(cls.getModifiers) || + cls.isInterface || + publicConstructorCount > 1 || + isModule != noPublicConstructors + if (definitelyNoTests) + None + else + fingerprints.find { + case f: SubclassFingerprint => + f.isModule == isModule && + loader.loadClass(f.superclassName()) + .isAssignableFrom(cls) + + case f: AnnotatedFingerprint => + val annotationCls = loader.loadClass(f.annotationName()) + .asInstanceOf[Class[Annotation]] + f.isModule == isModule && ( + cls.isAnnotationPresent(annotationCls) || + cls.getDeclaredMethods.exists(_.isAnnotationPresent(annotationCls)) || + cls.getMethods.exists { m => + m.isAnnotationPresent(annotationCls) && + Modifier.isPublic(m.getModifiers) + } + ) + } + } +} diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/Logger.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/Logger.scala new file mode 100644 index 0000000000..92986a0b48 --- /dev/null +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/Logger.scala @@ -0,0 +1,21 @@ +package scala.build.testrunner + +import java.io.PrintStream + +class Logger(val verbosity: Int, out: PrintStream) { + def error(message: String): Unit = out.println(message) + + def message(message: => String): Unit = if (verbosity >= 0) out.println(message) + + def log(message: => String): Unit = if (verbosity >= 1) out.println(message) + def log(message: => String, debugMessage: => String): Unit = + if (verbosity >= 2) out.println(debugMessage) + else if (verbosity >= 1) out.println(message) + + def debug(message: => String): Unit = if (verbosity >= 2) out.println(message) +} + +object Logger { + def apply(verbosity: Int, out: PrintStream) = new Logger(verbosity, out) + def apply(verbosity: Int) = new Logger(verbosity, out = System.err) +} diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala index 2dfd9ac240..3faab0b548 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala @@ -1,6 +1,6 @@ package scala.build.testrunner -import sbt.testing._ +import sbt.testing.{Logger => SbtTestLogger, _} import java.io.{File, PrintStream} import java.nio.file.{Path, Paths} @@ -9,10 +9,15 @@ import scala.collection.mutable object TestRunner { - def commonTestFrameworks = Seq( + def commonTestFrameworks: Seq[String] = Seq( "munit.Framework", "utest.runner.Framework", - "org.scalacheck.ScalaCheckFramework" + "org.scalacheck.ScalaCheckFramework", + "zio.test.sbt.ZTestFramework", + "org.scalatest.tools.Framework", + "com.novocode.junit.JUnitFramework", + "org.scalajs.junit.JUnitFramework", + "weaver.framework.CatsEffect" ) def classPath(loader: ClassLoader): Seq[Path] = { @@ -49,21 +54,17 @@ object TestRunner { val events = mutable.Buffer.empty[Event] - val logger: Logger = - new Logger { - def error(msg: String) = out.println(msg) - def warn(msg: String) = out.println(msg) - def info(msg: String) = out.println(msg) - def debug(msg: String) = out.println(msg) - def trace(t: Throwable) = t.printStackTrace(out) - def ansiCodesSupported() = true + val logger: SbtTestLogger = + new SbtTestLogger { + def error(msg: String): Unit = out.println(msg) + def warn(msg: String): Unit = out.println(msg) + def info(msg: String): Unit = out.println(msg) + def debug(msg: String): Unit = out.println(msg) + def trace(t: Throwable): Unit = t.printStackTrace(out) + def ansiCodesSupported(): Boolean = true } - val eventHandler: EventHandler = - new EventHandler { - def handle(event: Event) = - events.append(event) - } + val eventHandler: EventHandler = (event: Event) => events.append(event) while (tasks.nonEmpty) { val task = tasks.dequeue() diff --git a/project/settings.sc b/project/settings.sc index 7d6e125ea9..d9ee20e7fe 100644 --- a/project/settings.sc +++ b/project/settings.sc @@ -556,6 +556,8 @@ trait HasTests extends SbtModule { def repositoriesTask = T.task(super.repositoriesTask() ++ deps.customRepositories) + + override def testFramework: T[String] = super.testFramework } }