@@ -3,17 +3,21 @@ package scala.build.internal
33import coursier .jvm .Execve
44import org .scalajs .jsenv .jsdomnodejs .JSDOMNodeJSEnv
55import 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
78import sbt .testing .{Framework , Status }
89
910import java .io .File
1011import java .nio .file .{Files , Path , Paths }
1112
1213import scala .build .EitherCps .{either , value }
1314import scala .build .Logger
14- import scala .build .errors ._
15+ import scala .build .Ops .EitherSeqOps
16+ import scala .build .errors .*
1517import scala .build .internals .EnvVar
18+ import scala .build .testrunner .FrameworkUtils .*
1619import scala .build .testrunner .{AsmTestRunner , TestRunner }
20+ import scala .scalanative .testinterface .adapter .TestAdapter as ScalaNativeTestAdapter
1721import scala .util .{Failure , Properties , Success }
1822
1923object 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