Skip to content

Commit 1a5443a

Browse files
authored
Merge pull request #3621 from Gedochao/feature/run-multiple-test-frameworks
Run all found test frameworks, rather than just one
2 parents 2a7023e + c5fa727 commit 1a5443a

File tree

13 files changed

+665
-378
lines changed

13 files changed

+665
-378
lines changed

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

+159-121
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@ package scala.build.internal
33
import coursier.jvm.Execve
44
import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv
55
import org.scalajs.jsenv.nodejs.NodeJSEnv
6-
import org.scalajs.jsenv.{Input, RunConfig}
6+
import org.scalajs.jsenv.{Input, JSEnv, RunConfig}
7+
import org.scalajs.testing.adapter.TestAdapter as ScalaJsTestAdapter
78
import sbt.testing.{Framework, Status}
89

910
import java.io.File
1011
import java.nio.file.{Files, Path, Paths}
1112

1213
import scala.build.EitherCps.{either, value}
1314
import scala.build.Logger
14-
import scala.build.errors._
15+
import scala.build.Ops.EitherSeqOps
16+
import scala.build.errors.*
1517
import scala.build.internals.EnvVar
18+
import scala.build.testrunner.FrameworkUtils.*
1619
import scala.build.testrunner.{AsmTestRunner, TestRunner}
20+
import scala.scalanative.testinterface.adapter.TestAdapter as ScalaNativeTestAdapter
1721
import scala.util.{Failure, Properties, Success}
1822

1923
object Runner {
@@ -238,22 +242,20 @@ object Runner {
238242
sourceMap: Boolean = false,
239243
esModule: Boolean = false
240244
): Either[BuildException, Process] = either {
241-
242-
import logger.{log, debug}
243-
244-
val nodePath = value(findInPath("node").map(_.toString).toRight(NodeNotFoundError()))
245-
246-
if (!jsDom && allowExecve && Execve.available()) {
247-
245+
val nodePath: String =
246+
value(findInPath("node")
247+
.map(_.toString)
248+
.toRight(NodeNotFoundError()))
249+
if !jsDom && allowExecve && Execve.available() then {
248250
val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args
249251

250-
log(
252+
logger.log(
251253
s"Running ${command.mkString(" ")}",
252254
" Running" + System.lineSeparator() +
253255
command.iterator.map(_ + System.lineSeparator()).mkString
254256
)
255257

256-
debug("execve available")
258+
logger.debug("execve available")
257259
Execve.execve(
258260
command.head,
259261
"node" +: command.tail.toArray,
@@ -262,40 +264,36 @@ object Runner {
262264
sys.error("should not happen")
263265
}
264266
else {
265-
266267
val nodeArgs =
267268
// Scala.js runs apps by piping JS to node.
268269
// If we need to pass arguments, we must first make the piped input explicit
269270
// with "-", and we pass the user's arguments after that.
270-
if (args.isEmpty) Nil
271-
else "-" :: args.toList
271+
if args.isEmpty then Nil else "-" :: args.toList
272272
val envJs =
273-
if (jsDom)
273+
if jsDom then
274274
new JSDOMNodeJSEnv(
275275
JSDOMNodeJSEnv.Config()
276276
.withExecutable(nodePath)
277277
.withArgs(nodeArgs)
278278
.withEnv(Map.empty)
279279
)
280-
else new NodeJSEnv(
281-
NodeJSEnv.Config()
282-
.withExecutable(nodePath)
283-
.withArgs(nodeArgs)
284-
.withEnv(Map.empty)
285-
.withSourceMap(sourceMap)
286-
)
280+
else
281+
new NodeJSEnv(
282+
NodeJSEnv.Config()
283+
.withExecutable(nodePath)
284+
.withArgs(nodeArgs)
285+
.withEnv(Map.empty)
286+
.withSourceMap(sourceMap)
287+
)
287288

288-
val inputs = Seq(
289-
if (esModule) Input.ESModule(entrypoint.toPath)
290-
else Input.Script(entrypoint.toPath)
291-
)
289+
val inputs =
290+
Seq(if esModule then Input.ESModule(entrypoint.toPath) else Input.Script(entrypoint.toPath))
292291

293292
val config = RunConfig().withLogger(logger.scalaJsLogger)
294293
val processJs = envJs.start(inputs, config)
295294

296295
processJs.future.value.foreach {
297-
case Failure(t) =>
298-
throw new Exception(t)
296+
case Failure(t) => throw new Exception(t)
299297
case Success(_) =>
300298
}
301299

@@ -346,32 +344,30 @@ object Runner {
346344

347345
private def runTests(
348346
classPath: Seq[Path],
349-
framework: Framework,
347+
frameworks: Seq[Framework],
350348
requireTests: Boolean,
351349
args: Seq[String],
352350
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 {
351+
): Either[NoTestsRun, Boolean] = frameworks
352+
.flatMap { framework =>
353+
val taskDefs =
354+
AsmTestRunner.taskDefs(
355+
classPath,
356+
keepJars = false,
357+
framework.fingerprints().toIndexedSeq,
358+
parentInspector
359+
).toArray
360+
361+
val runner = framework.runner(args.toArray, Array(), null)
362+
val initialTasks = runner.tasks(taskDefs)
363+
val events = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out)
364+
365+
val doneMsg = runner.done()
366+
if doneMsg.nonEmpty then System.out.println(doneMsg)
367+
events
368+
} match {
369+
case events if requireTests && events.isEmpty => Left(new NoTestsRun)
370+
case events => Right {
375371
!events.exists { ev =>
376372
ev.status == Status.Error ||
377373
ev.status == Status.Failure ||
@@ -380,22 +376,30 @@ object Runner {
380376
}
381377
}
382378

383-
def frameworkName(
379+
def frameworkNames(
384380
classPath: Seq[Path],
385-
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-
}
381+
parentInspector: AsmTestRunner.ParentInspector,
382+
logger: Logger
383+
): Either[NoTestFrameworkFoundError, Seq[String]] = {
384+
logger.debug("Looking for test framework services on the classpath...")
385+
val foundFrameworkServices =
386+
AsmTestRunner.findFrameworkServices(classPath)
387+
.map(_.replace('/', '.').replace('\\', '.'))
388+
logger.debug(s"Found ${foundFrameworkServices.length} test framework services.")
389+
if foundFrameworkServices.nonEmpty then
390+
logger.debug(s" - ${foundFrameworkServices.mkString("\n - ")}")
391+
logger.debug("Looking for more test frameworks on the classpath...")
392+
val foundFrameworks =
393+
AsmTestRunner.findFrameworks(classPath, TestRunner.commonTestFrameworks, parentInspector)
394+
.map(_.replace('/', '.').replace('\\', '.'))
395+
logger.debug(s"Found ${foundFrameworks.length} additional test frameworks")
396+
if foundFrameworks.nonEmpty then
397+
logger.debug(s" - ${foundFrameworks.mkString("\n - ")}")
398+
val frameworks: Seq[String] = foundFrameworkServices ++ foundFrameworks
399+
logger.log(s"Found ${frameworks.length} test frameworks in total")
400+
if frameworks.nonEmpty then
401+
logger.debug(s" - ${frameworks.mkString("\n - ")}")
402+
if frameworks.nonEmpty then Right(frameworks) else Left(new NoTestFrameworkFoundError)
399403
}
400404

401405
def testJs(
@@ -410,57 +414,72 @@ object Runner {
410414
): Either[TestError, Int] = either {
411415
import org.scalajs.jsenv.Input
412416
import org.scalajs.jsenv.nodejs.NodeJSEnv
413-
import org.scalajs.testing.adapter.TestAdapter
417+
logger.debug("Preparing to run tests with Scala.js...")
418+
logger.debug(s"Scala.js tests class path: $classPath")
414419
val nodePath = findInPath("node").fold("node")(_.toString)
415-
val jsEnv =
416-
if (jsDom)
420+
logger.debug(s"Node found at $nodePath")
421+
val jsEnv: JSEnv =
422+
if jsDom then {
423+
logger.log("Loading JS environment with JS DOM...")
417424
new JSDOMNodeJSEnv(
418425
JSDOMNodeJSEnv.Config()
419426
.withExecutable(nodePath)
420427
.withArgs(Nil)
421428
.withEnv(Map.empty)
422429
)
423-
else new NodeJSEnv(
424-
NodeJSEnv.Config()
425-
.withExecutable(nodePath)
426-
.withArgs(Nil)
427-
.withEnv(Map.empty)
428-
.withSourceMap(NodeJSEnv.SourceMap.Disable)
429-
)
430-
val adapterConfig = TestAdapter.Config().withLogger(logger.scalaJsLogger)
431-
val inputs = Seq(
432-
if (esModule) Input.ESModule(entrypoint.toPath)
433-
else Input.Script(entrypoint.toPath)
434-
)
435-
var adapter: TestAdapter = null
430+
}
431+
else {
432+
logger.log("Loading JS environment with Node...")
433+
new NodeJSEnv(
434+
NodeJSEnv.Config()
435+
.withExecutable(nodePath)
436+
.withArgs(Nil)
437+
.withEnv(Map.empty)
438+
.withSourceMap(NodeJSEnv.SourceMap.Disable)
439+
)
440+
}
441+
val adapterConfig = ScalaJsTestAdapter.Config().withLogger(logger.scalaJsLogger)
442+
val inputs =
443+
Seq(if esModule then Input.ESModule(entrypoint.toPath) else Input.Script(entrypoint.toPath))
444+
var adapter: ScalaJsTestAdapter = null
436445

437446
logger.debug(s"JS tests class path: $classPath")
438447

439448
val parentInspector = new AsmTestRunner.ParentInspector(classPath)
440-
val frameworkName0 = testFrameworkOpt match {
441-
case Some(fw) => fw
442-
case None => value(frameworkName(classPath, parentInspector))
449+
val foundFrameworkNames: List[String] = testFrameworkOpt match {
450+
case some @ Some(_) => some.toList
451+
case None => value(frameworkNames(classPath, parentInspector, logger)).toList
443452
}
444453

445454
val res =
446455
try {
447-
adapter = new TestAdapter(jsEnv, inputs, adapterConfig)
448-
449-
val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten
456+
adapter = new ScalaJsTestAdapter(jsEnv, inputs, adapterConfig)
457+
458+
val loadedFrameworks =
459+
adapter
460+
.loadFrameworks(foundFrameworkNames.map(List(_)))
461+
.flatten
462+
.distinctBy(_.name())
463+
464+
val finalTestFrameworks =
465+
loadedFrameworks
466+
.filter(
467+
!_.name().toLowerCase.contains("junit") ||
468+
!loadedFrameworks.exists(_.name().toLowerCase.contains("munit"))
469+
)
470+
if finalTestFrameworks.nonEmpty then
471+
logger.log(
472+
s"""Final list of test frameworks found:
473+
| - ${finalTestFrameworks.map(_.description).mkString("\n - ")}
474+
|""".stripMargin
475+
)
450476

451-
if (frameworks.isEmpty)
452-
Left(new NoFrameworkFoundByBridgeError)
453-
else if (frameworks.length > 1)
454-
Left(new TooManyFrameworksFoundByBridgeError)
455-
else {
456-
val framework = frameworks.head
457-
runTests(classPath, framework, requireTests, args, parentInspector)
458-
}
477+
if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError)
478+
else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector)
459479
}
460-
finally if (adapter != null) adapter.close()
480+
finally if adapter != null then adapter.close()
461481

462-
if (value(res)) 0
463-
else 1
482+
if value(res) then 0 else 1
464483
}
465484

466485
def testNative(
@@ -471,42 +490,61 @@ object Runner {
471490
args: Seq[String],
472491
logger: Logger
473492
): Either[TestError, Int] = either {
474-
475-
import scala.scalanative.testinterface.adapter.TestAdapter
476-
493+
logger.debug("Preparing to run tests with Scala Native...")
477494
logger.debug(s"Native tests class path: $classPath")
478495

479496
val parentInspector = new AsmTestRunner.ParentInspector(classPath)
480-
val frameworkName0 = frameworkNameOpt match {
481-
case Some(fw) => fw
482-
case None => value(frameworkName(classPath, parentInspector))
497+
val foundFrameworkNames: List[String] = frameworkNameOpt match {
498+
case Some(fw) => List(fw)
499+
case None => value(frameworkNames(classPath, parentInspector, logger)).toList
483500
}
484501

485-
val config = TestAdapter.Config()
502+
val config = ScalaNativeTestAdapter.Config()
486503
.withBinaryFile(launcher)
487-
.withEnvVars(sys.env.toMap)
504+
.withEnvVars(sys.env)
488505
.withLogger(logger.scalaNativeTestLogger)
489506

490-
var adapter: TestAdapter = null
507+
var adapter: ScalaNativeTestAdapter = null
491508

492509
val res =
493510
try {
494-
adapter = new TestAdapter(config)
511+
adapter = new ScalaNativeTestAdapter(config)
512+
513+
val loadedFrameworks =
514+
adapter
515+
.loadFrameworks(foundFrameworkNames.map(List(_)))
516+
.flatten
517+
.distinctBy(_.name())
518+
519+
val finalTestFrameworks =
520+
loadedFrameworks
521+
// .filter(
522+
// _.name() != "Scala Native JUnit test framework" ||
523+
// !loadedFrameworks.exists(_.name() == "munit")
524+
// )
525+
// TODO: add support for JUnit and then only hardcode filtering it out when passed with munit
526+
// https://github.com/VirtusLab/scala-cli/issues/3627
527+
.filter(_.name() != "Scala Native JUnit test framework")
528+
if finalTestFrameworks.nonEmpty then
529+
logger.log(
530+
s"""Final list of test frameworks found:
531+
| - ${finalTestFrameworks.map(_.description).mkString("\n - ")}
532+
|""".stripMargin
533+
)
495534

496-
val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten
535+
val skippedFrameworks = loadedFrameworks.diff(finalTestFrameworks)
536+
if skippedFrameworks.nonEmpty then
537+
logger.log(
538+
s"""The following test frameworks have been filtered out:
539+
| - ${skippedFrameworks.map(_.description).mkString("\n - ")}
540+
|""".stripMargin
541+
)
497542

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-
}
543+
if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError)
544+
else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector)
506545
}
507-
finally if (adapter != null) adapter.close()
546+
finally if adapter != null then adapter.close()
508547

509-
if (value(res)) 0
510-
else 1
548+
if value(res) then 0 else 1
511549
}
512550
}

0 commit comments

Comments
 (0)