Skip to content

Commit 2554327

Browse files
committed
Run all found test frameworks, rather than just one (with Scala Native)
1 parent 203c967 commit 2554327

File tree

11 files changed

+186
-134
lines changed

11 files changed

+186
-134
lines changed

modules/build/src/main/scala/scala/build/internal/Runner.scala

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import java.nio.file.{Files, Path, Paths}
1111

1212
import scala.build.EitherCps.{either, value}
1313
import scala.build.Logger
14-
import scala.build.errors._
14+
import scala.build.Ops.EitherSeqOps
15+
import scala.build.errors.*
1516
import scala.build.internals.EnvVar
17+
import scala.build.testrunner.FrameworkUtils.*
1618
import scala.build.testrunner.{AsmTestRunner, TestRunner}
1719
import scala.util.{Failure, Properties, Success}
1820

@@ -346,32 +348,30 @@ object Runner {
346348

347349
private def runTests(
348350
classPath: Seq[Path],
349-
framework: Framework,
351+
frameworks: Seq[Framework],
350352
requireTests: Boolean,
351353
args: Seq[String],
352354
parentInspector: AsmTestRunner.ParentInspector
353-
): Either[NoTestsRun, Boolean] = {
354-
355-
val taskDefs =
356-
AsmTestRunner.taskDefs(
357-
classPath,
358-
keepJars = false,
359-
framework.fingerprints().toIndexedSeq,
360-
parentInspector
361-
).toArray
362-
363-
val runner = framework.runner(args.toArray, Array(), null)
364-
val initialTasks = runner.tasks(taskDefs)
365-
val events = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out)
366-
367-
val doneMsg = runner.done()
368-
if (doneMsg.nonEmpty)
369-
System.out.println(doneMsg)
370-
371-
if (requireTests && events.isEmpty)
372-
Left(new NoTestsRun)
373-
else
374-
Right {
355+
): Either[NoTestsRun, Boolean] = frameworks
356+
.flatMap { framework =>
357+
val taskDefs =
358+
AsmTestRunner.taskDefs(
359+
classPath,
360+
keepJars = false,
361+
framework.fingerprints().toIndexedSeq,
362+
parentInspector
363+
).toArray
364+
365+
val runner = framework.runner(args.toArray, Array(), null)
366+
val initialTasks = runner.tasks(taskDefs)
367+
val events = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out)
368+
369+
val doneMsg = runner.done()
370+
if doneMsg.nonEmpty then System.out.println(doneMsg)
371+
events
372+
} match {
373+
case events if requireTests && events.isEmpty => Left(new NoTestsRun)
374+
case events => Right {
375375
!events.exists { ev =>
376376
ev.status == Status.Error ||
377377
ev.status == Status.Failure ||
@@ -380,22 +380,17 @@ object Runner {
380380
}
381381
}
382382

383-
def frameworkName(
383+
def frameworkNames(
384384
classPath: Seq[Path],
385385
parentInspector: AsmTestRunner.ParentInspector
386-
): Either[NoTestFrameworkFoundError, String] = {
387-
val fwOpt = AsmTestRunner.findFrameworkService(classPath)
388-
.orElse {
389-
AsmTestRunner.findFramework(
390-
classPath,
391-
TestRunner.commonTestFrameworks,
392-
parentInspector
393-
)
394-
}
395-
fwOpt match {
396-
case Some(fw) => Right(fw.replace('/', '.').replace('\\', '.'))
397-
case None => Left(new NoTestFrameworkFoundError)
398-
}
386+
): Either[NoTestFrameworkFoundError, Seq[String]] = {
387+
val foundFrameworkServices = AsmTestRunner.findFrameworkServices(classPath)
388+
val foundFrameworks =
389+
AsmTestRunner.findFrameworks(classPath, TestRunner.commonTestFrameworks, parentInspector)
390+
val frameworks: Seq[String] =
391+
(foundFrameworkServices ++ foundFrameworks)
392+
.map(_.replace('/', '.').replace('\\', '.'))
393+
if frameworks.nonEmpty then Right(frameworks) else Left(new NoTestFrameworkFoundError)
399394
}
400395

401396
def testJs(
@@ -437,9 +432,9 @@ object Runner {
437432
logger.debug(s"JS tests class path: $classPath")
438433

439434
val parentInspector = new AsmTestRunner.ParentInspector(classPath)
440-
val frameworkName0 = testFrameworkOpt match {
435+
val frameworkName0: String = testFrameworkOpt match {
441436
case Some(fw) => fw
442-
case None => value(frameworkName(classPath, parentInspector))
437+
case None => value(frameworkNames(classPath, parentInspector)).head
443438
}
444439

445440
val res =
@@ -454,7 +449,7 @@ object Runner {
454449
Left(new TooManyFrameworksFoundByBridgeError)
455450
else {
456451
val framework = frameworks.head
457-
runTests(classPath, framework, requireTests, args, parentInspector)
452+
runTests(classPath, Seq(framework), requireTests, args, parentInspector)
458453
}
459454
}
460455
finally if (adapter != null) adapter.close()
@@ -477,9 +472,9 @@ object Runner {
477472
logger.debug(s"Native tests class path: $classPath")
478473

479474
val parentInspector = new AsmTestRunner.ParentInspector(classPath)
480-
val frameworkName0 = frameworkNameOpt match {
481-
case Some(fw) => fw
482-
case None => value(frameworkName(classPath, parentInspector))
475+
val foundFrameworkNames: List[String] = frameworkNameOpt match {
476+
case Some(fw) => List(fw)
477+
case None => value(frameworkNames(classPath, parentInspector)).toList
483478
}
484479

485480
val config = TestAdapter.Config()
@@ -493,20 +488,41 @@ object Runner {
493488
try {
494489
adapter = new TestAdapter(config)
495490

496-
val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten
491+
val loadedFrameworks =
492+
adapter
493+
.loadFrameworks(foundFrameworkNames.map(List(_)))
494+
.flatten
495+
.distinctBy(_.name())
496+
497+
val finalTestFrameworks =
498+
loadedFrameworks
499+
// .filter(
500+
// _.name() != "Scala Native JUnit test framework" ||
501+
// !loadedFrameworks.exists(_.name() == "munit")
502+
// )
503+
// TODO: add support for JUnit and then only hardcode filtering it out when passed with munit
504+
// https://github.com/VirtusLab/scala-cli/issues/3627
505+
.filter(_.name() != "Scala Native JUnit test framework")
506+
if finalTestFrameworks.nonEmpty then
507+
logger.log(
508+
s"""Final list of test frameworks found:
509+
| - ${finalTestFrameworks.map(_.description).mkString("\n - ")}
510+
|""".stripMargin
511+
)
497512

498-
if (frameworks.isEmpty)
499-
Left(new NoFrameworkFoundByBridgeError)
500-
else if (frameworks.length > 1)
501-
Left(new TooManyFrameworksFoundByBridgeError)
502-
else {
503-
val framework = frameworks.head
504-
runTests(classPath, framework, requireTests, args, parentInspector)
505-
}
513+
val skippedFrameworks = loadedFrameworks.diff(finalTestFrameworks)
514+
if skippedFrameworks.nonEmpty then
515+
logger.log(
516+
s"""The following test frameworks have been filtered out:
517+
| - ${skippedFrameworks.map(_.description).mkString("\n - ")}
518+
|""".stripMargin
519+
)
520+
521+
if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError)
522+
else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector)
506523
}
507-
finally if (adapter != null) adapter.close()
524+
finally if adapter != null then adapter.close()
508525

509-
if (value(res)) 0
510-
else 1
526+
if value(res) then 0 else 1
511527
}
512528
}

modules/cli/src/main/scala/scala/cli/commands/test/Test.scala

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,25 +266,23 @@ object Test extends ScalaCommand[TestOptions] {
266266
}
267267
}
268268

269-
def findTestFramework(classPath: Seq[Path], logger: Logger): Option[String] = {
269+
private def findTestFramework(classPath: Seq[Path], logger: Logger): Option[String] = {
270270
val classPath0 = classPath.map(_.toString)
271271

272272
// https://github.com/VirtusLab/scala-cli/issues/426
273-
if (
274-
classPath0.exists(_.contains("zio-test")) && !classPath0.exists(_.contains("zio-test-sbt"))
275-
) {
273+
if classPath0.exists(_.contains("zio-test")) && !classPath0.exists(_.contains("zio-test-sbt"))
274+
then {
276275
val parentInspector = new AsmTestRunner.ParentInspector(classPath)
277-
Runner.frameworkName(classPath, parentInspector) match {
278-
case Right(f) => Some(f)
276+
Runner.frameworkNames(classPath, parentInspector) match {
277+
case Right(f) => f.headOption
279278
case Left(_) =>
280279
logger.message(
281280
s"zio-test found in the class path, zio-test-sbt should be added to run zio tests with $fullRunnerName."
282281
)
283282
None
284283
}
285284
}
286-
else
287-
None
285+
else None
288286
}
289287

290288
}

modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import java.nio.file.Path
1313
import scala.build.errors.BuildException
1414
import scala.build.info.{BuildInfo, ScopedBuildInfo}
1515
import scala.build.internal.Constants
16-
import scala.build.internal.Runner.frameworkName
1716
import scala.build.options.{BuildOptions, Scope}
1817
import scala.build.testrunner.AsmTestRunner
1918
import scala.build.{Logger, Positioned, Sources}

modules/cli/src/main/scala/scala/cli/exportCmd/MavenProjectDescriptor.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import java.nio.file.Path
1010

1111
import scala.build.errors.BuildException
1212
import scala.build.internal.Constants
13-
import scala.build.internal.Runner.frameworkName
13+
import scala.build.internal.Runner.frameworkNames
1414
import scala.build.options.{BuildOptions, Platform, Scope, ShadowingSeq}
1515
import scala.build.testrunner.AsmTestRunner
1616
import scala.build.{Logger, Positioned, Sources}

modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import java.nio.file.Path
99

1010
import scala.build.errors.BuildException
1111
import scala.build.internal.Constants
12-
import scala.build.internal.Runner.frameworkName
12+
import scala.build.internal.Runner.frameworkNames
1313
import scala.build.options.{BuildOptions, Platform, ScalaJsOptions, ScalaNativeOptions, Scope}
1414
import scala.build.testrunner.AsmTestRunner
1515
import scala.build.{Logger, Sources}
@@ -149,7 +149,8 @@ final case class MillProjectDescriptor(
149149
}
150150
val parentInspector = new AsmTestRunner.ParentInspector(testClassPath)
151151
val frameworkName0 = options.testOptions.frameworkOpt.orElse {
152-
frameworkName(testClassPath, parentInspector).toOption
152+
frameworkNames(testClassPath, parentInspector).toOption
153+
.flatMap(_.headOption) // TODO: handle multiple frameworks here
153154
}
154155

155156
val testFrameworkDecls = frameworkName0 match {

modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import java.nio.file.Path
1010

1111
import scala.build.errors.BuildException
1212
import scala.build.internal.Constants
13-
import scala.build.internal.Runner.frameworkName
13+
import scala.build.internal.Runner.frameworkNames
1414
import scala.build.options.{
1515
BuildOptions,
1616
Platform,
@@ -262,7 +262,8 @@ final case class SbtProjectDescriptor(
262262

263263
val parentInspector = new AsmTestRunner.ParentInspector(testClassPath)
264264
val frameworkName0 = options.testOptions.frameworkOpt.orElse {
265-
frameworkName(testClassPath, parentInspector).toOption
265+
frameworkNames(testClassPath, parentInspector).toOption
266+
.flatMap(_.headOption) // TODO: handle multiple frameworks here
266267
}
267268

268269
val testFrameworkSettings = frameworkName0 match {

modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr
513513
Seq("--native-version", "0.4.17")
514514
else Nil
515515
val baseRes =
516-
os.proc(TestUtil.cli, "test", extraOptions, platformArgs, scalaTestExtraArgs, ".")
516+
os.proc(TestUtil.cli, "test", extraOptions, platformArgs, scalaTestExtraArgs, ".", "-v")
517517
.call(cwd = root, check = false)
518518
if (baseRes.exitCode != 0) {
519519
println(baseRes.out.text())
@@ -811,42 +811,66 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr
811811
}
812812
}
813813

814-
test("multiple test frameworks") {
815-
val scalatestMessage = "Hello from ScalaTest"
816-
val munitMessage = "Hello from Munit"
817-
TestInputs(
818-
os.rel / "project.scala" ->
819-
s"""//> using test.dep org.scalatest::scalatest::3.2.19
820-
|//> using test.dep org.scalameta::munit::$munitVersion
821-
|""".stripMargin,
822-
os.rel / "scalatest.test.scala" ->
823-
s"""import org.scalatest.flatspec.AnyFlatSpec
824-
|
825-
|class ScalaTestSpec extends AnyFlatSpec {
826-
| "example" should "work" in {
827-
| assertResult(1)(1)
828-
| println("$scalatestMessage")
829-
| }
830-
|}
831-
|""".stripMargin,
832-
os.rel / "munit.test.scala" ->
833-
s"""import munit.FunSuite
834-
|
835-
|class Munit extends FunSuite {
836-
| test("foo") {
837-
| assert(2 + 2 == 4)
838-
| println("$munitMessage")
839-
| }
840-
|}
841-
|""".stripMargin
842-
).fromRoot { root =>
843-
val r = os.proc(TestUtil.cli, "test", extraOptions, ".").call(cwd = root)
844-
val output = r.out.trim()
845-
expect(output.nonEmpty)
846-
expect(output.contains(scalatestMessage))
847-
expect(countSubStrings(output, scalatestMessage) == 1)
848-
expect(output.contains(munitMessage))
849-
expect(countSubStrings(output, munitMessage) == 1)
850-
}
814+
for {
815+
platformOptions <- Seq(
816+
Nil, // JVM
817+
Seq("--native")
818+
// TODO: Seq("--js")
819+
)
820+
platformDescription = platformOptions.headOption.map(o => s" ($o)").getOrElse(" (JVM)")
851821
}
822+
test(s"multiple test frameworks$platformDescription") {
823+
val scalatestMessage = "Hello from ScalaTest"
824+
val munitMessage = "Hello from Munit"
825+
val utestMessage = "Hello from utest"
826+
TestInputs(
827+
os.rel / "project.scala" ->
828+
s"""//> using test.dep org.scalatest::scalatest::3.2.19
829+
|//> using test.dep org.scalameta::munit::$munitVersion
830+
|//> using dep com.lihaoyi::utest::$utestVersion
831+
|""".stripMargin,
832+
os.rel / "scalatest.test.scala" ->
833+
s"""import org.scalatest.flatspec.AnyFlatSpec
834+
|
835+
|class ScalaTestSpec extends AnyFlatSpec {
836+
| "example" should "work" in {
837+
| assertResult(1)(1)
838+
| println("$scalatestMessage")
839+
| }
840+
|}
841+
|""".stripMargin,
842+
os.rel / "munit.test.scala" ->
843+
s"""import munit.FunSuite
844+
|
845+
|class Munit extends FunSuite {
846+
| test("foo") {
847+
| assert(2 + 2 == 4)
848+
| println("$munitMessage")
849+
| }
850+
|}
851+
|""".stripMargin,
852+
os.rel / "utest.test.scala" ->
853+
s"""import utest._
854+
|
855+
|object MyTests extends TestSuite {
856+
| val tests = Tests {
857+
| test("foo") {
858+
| assert(2 + 2 == 4)
859+
| println("$utestMessage")
860+
| }
861+
| }
862+
|}""".stripMargin
863+
).fromRoot { root =>
864+
val r = os.proc(TestUtil.cli, "test", extraOptions, ".", platformOptions).call(cwd = root)
865+
val output = r.out.trim()
866+
expect(output.nonEmpty)
867+
expect(output.contains(scalatestMessage))
868+
expect(countSubStrings(output, scalatestMessage) == 1)
869+
expect(output.contains(munitMessage))
870+
expect(countSubStrings(output, munitMessage) == 1)
871+
expect(output.contains(utestMessage))
872+
expect(countSubStrings(output, utestMessage) == 1)
873+
println(output)
874+
}
875+
}
852876
}

0 commit comments

Comments
 (0)