diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea04cfd..a020ac9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,15 @@ jobs: strategy: fail-fast: false matrix: + os: [ ubuntu-latest ] + java: [ "8", "11", "21" ] include: - - os: ubuntu-latest - java: 8 - jobtype: 1 - - os: ubuntu-latest - java: 11 - jobtype: 1 + - java: 8 + distribution: zulu + - java: 11 + distribution: temurin + - java: 21 + distribution: temurin runs-on: ${{ matrix.os }} env: # define Java options for both official sbt and sbt-extras @@ -23,10 +25,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup - uses: olafurpg/setup-scala@v10 + - name: Setup JDK + uses: actions/setup-java@v5 with: - java-version: "adopt@1.${{ matrix.java }}" + distribution: "${{ matrix.distribution }}" + java-version: "${{ matrix.java }}" + cache: sbt + - name: Setup sbt + uses: sbt/setup-sbt@v1 - name: Coursier cache uses: coursier/cache-action@v6 - name: Build and test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04474f1..3d49b6f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,10 +13,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup - uses: olafurpg/setup-scala@v10 + - name: Setup JDK + uses: actions/setup-java@v5 with: - java-version: "adopt@1.8" + distribution: "zulu" + java-version: 8 + cache: sbt + - name: Setup sbt + uses: sbt/setup-sbt@v1 - name: Coursier cache uses: coursier/cache-action@v5 - name: Test diff --git a/.gitignore b/.gitignore index 0603afb..58fbec6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ target .bloop/ .bsp/ .vscode/ +metals.sbt diff --git a/project/build.properties b/project/build.properties index 19479ba..f0fdcab 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.5.2 +sbt.version = 1.11.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index 32e1cb8..17edda6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.0") -addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") +addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.2") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") diff --git a/src/main/java/com/novocode/junit/JUnitTask.java b/src/main/java/com/novocode/junit/JUnitTask.java index 9e16064..b086565 100644 --- a/src/main/java/com/novocode/junit/JUnitTask.java +++ b/src/main/java/com/novocode/junit/JUnitTask.java @@ -2,10 +2,7 @@ import junit.framework.TestCase; import org.junit.experimental.categories.Categories; -import org.junit.runner.Description; -import org.junit.runner.JUnitCore; -import org.junit.runner.Request; -import org.junit.runner.RunWith; +import org.junit.runner.*; import sbt.testing.*; import java.lang.annotation.Annotation; @@ -61,10 +58,39 @@ public Task[] execute(EventHandler eventHandler, Logger[] loggers) { Categories.CategoryFilter.categoryFilter(true, loadClasses(runner.testClassLoader, settings.includeCategories), true, loadClasses(runner.testClassLoader, settings.excludeCategories))); } - ju.run(request); - } + // If the resulting request yields zero atomic test descriptions, treat as empty suite. + // Occurs with certain custom @RunWith runners that deliberately don't expose + // atomic tests (e.g., capability matrix collapses to nothing). We short-circuit + // to a graceful empty run to avoid downstream AIOOBEs + Description rootDesc; + try { + rootDesc = request.getRunner().getDescription(); + } catch (Exception e) { + logger.warn("Unable to obtain JUnit description for " + testClassName + ": " + e); + // Uniform lifecycle even on exception path + ed.testRunStarted(taskDescription); + ed.testExecutionFailed(testClassName, e); // report failure during the run + Result result = new Result(); + ed.testRunFinished(result); + return new Task[0]; + } + if (isEffectivelyEmpty(rootDesc)) { + logger.debug("Suite " + testClassName + " contains no atomic tests – treating as empty."); + // Emit start/finish so stats remain consistent, but no test events. + Description startDesc = (rootDesc != null ? rootDesc : taskDescription); + ed.testRunStarted(startDesc); + Result result = new Result(); + ed.testRunFinished(result); + } else { + ju.run(request); + } + } } catch(Exception ex) { - ed.testExecutionFailed(testClassName, ex); + // Uniform lifecycle even on exception path + ed.testRunStarted(taskDescription); + ed.testExecutionFailed(testClassName, ex); + Result result = new Result(); + ed.testRunFinished(result); } } finally { settings.restoreSystemProperties(oldprops); @@ -72,6 +98,26 @@ public Task[] execute(EventHandler eventHandler, Logger[] loggers) { return new Task[0]; // junit tests do not nest } + private static boolean isEffectivelyEmpty(Description root) { + if (root == null) return true; + if (root.isTest()) return false; // Root is an atomic test + List children = root.getChildren(); + if (children.isEmpty()) return true; // Non-test node, no children → empty + Deque q = new ArrayDeque<>(); + for (Description child : children) { + if (child.isTest()) return false; + q.addLast(child); + } + while (!q.isEmpty()) { + Description d = q.removeFirst(); + for (Description child : d.getChildren()) { + if (child.isTest()) return false; + q.addLast(child); + } + } + return true; // no test nodes found + } + private boolean shouldRun(Fingerprint fingerprint, Class clazz, RunSettings settings) { if(JUNIT_FP.equals(fingerprint)) { // Ignore classes which are matched by the other fingerprints diff --git a/src/sbt-test/simple/can-run-single-test/build.sbt b/src/sbt-test/simple/can-run-single-test/build.sbt index b0669ed..9b67a18 100644 --- a/src/sbt-test/simple/can-run-single-test/build.sbt +++ b/src/sbt-test/simple/can-run-single-test/build.sbt @@ -1,6 +1,6 @@ name := "test-project" -scalaVersion := "2.10.7" +scalaVersion := "2.11.12" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" diff --git a/src/sbt-test/simple/categories/build.sbt b/src/sbt-test/simple/categories/build.sbt index be36536..7952de9 100644 --- a/src/sbt-test/simple/categories/build.sbt +++ b/src/sbt-test/simple/categories/build.sbt @@ -1,6 +1,6 @@ name := "test-project" -scalaVersion := "2.10.7" +scalaVersion := "2.11.12" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" diff --git a/src/sbt-test/simple/check-test-selector/build.sbt b/src/sbt-test/simple/check-test-selector/build.sbt index f4acffc..0b61aee 100644 --- a/src/sbt-test/simple/check-test-selector/build.sbt +++ b/src/sbt-test/simple/check-test-selector/build.sbt @@ -1,6 +1,6 @@ name := "test-project" -scalaVersion := "2.13.7" +scalaVersion := "2.13.16" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" libraryDependencies += "org.scala-sbt" % "test-agent" % "1.5.5" % Test diff --git a/src/sbt-test/simple/check-test-selector/project/build.properties b/src/sbt-test/simple/check-test-selector/project/build.properties deleted file mode 100644 index 3161d21..0000000 --- a/src/sbt-test/simple/check-test-selector/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.6.1 diff --git a/src/sbt-test/simple/empty-custom-runner/build.sbt b/src/sbt-test/simple/empty-custom-runner/build.sbt new file mode 100644 index 0000000..10d583d --- /dev/null +++ b/src/sbt-test/simple/empty-custom-runner/build.sbt @@ -0,0 +1,24 @@ +name := "empty-custom-runner" + +scalaVersion := "2.13.16" + +libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % Test + +Test / fork := false + +val checkEmptyRun = taskKey[Unit]("Check that the run completed with 0 tests") + +checkEmptyRun := { + // Simple approach: just verify the test class exists and can be instantiated + // The real verification is that testOnly didn't crash (which we test in the scripted sequence) + val testClass = (Test / classDirectory).value / "JSEnvMatrixSuite.class" + assert(testClass.exists(), "Test class should have compiled successfully") + + // Also verify the runner class compiled + val runnerClass = (Test / classDirectory).value / "EmptyMatrixRunner.class" + assert(runnerClass.exists(), "Custom runner should have compiled successfully") + + // If we reach this point, the compilation succeeded and testOnly didn't crash + // which means our fix is working + streams.value.log.info("Empty custom runner test completed successfully - no crash occurred") +} diff --git a/src/sbt-test/simple/empty-custom-runner/src/test/scala/EmptyMatrixRunner.scala b/src/sbt-test/simple/empty-custom-runner/src/test/scala/EmptyMatrixRunner.scala new file mode 100644 index 0000000..7c592ba --- /dev/null +++ b/src/sbt-test/simple/empty-custom-runner/src/test/scala/EmptyMatrixRunner.scala @@ -0,0 +1,13 @@ +import org.junit.runner.{Description, Runner} +import org.junit.runner.notification.RunNotifier + +// Simulates a parameterized runner that has no atomic tests at discovery time. +// getDescription returns a root Description with zero children. +final class EmptyMatrixRunner(cls: Class[_]) extends Runner { + private val root: Description = Description.createSuiteDescription(cls) + + override def getDescription(): Description = root + + // Nothing to run + override def run(notifier: RunNotifier): Unit = () +} \ No newline at end of file diff --git a/src/sbt-test/simple/empty-custom-runner/src/test/scala/JSEnvMatrixSuite.scala b/src/sbt-test/simple/empty-custom-runner/src/test/scala/JSEnvMatrixSuite.scala new file mode 100644 index 0000000..98c248c --- /dev/null +++ b/src/sbt-test/simple/empty-custom-runner/src/test/scala/JSEnvMatrixSuite.scala @@ -0,0 +1,5 @@ +import org.junit.runner.RunWith + +// A test suite using the custom runner which reports no atomic tests. +@RunWith(classOf[EmptyMatrixRunner]) +class JSEnvMatrixSuite diff --git a/src/sbt-test/simple/empty-custom-runner/test b/src/sbt-test/simple/empty-custom-runner/test new file mode 100644 index 0000000..df3bd59 --- /dev/null +++ b/src/sbt-test/simple/empty-custom-runner/test @@ -0,0 +1,11 @@ +# Running a @RunWith suite whose description tree has zero atomic children +# should NOT crash; it should be treated as an empty run with 0 tests. + +# Compile the test classes first +> Test/compile + +# Run only the suite - this should not crash +> testOnly *JSEnvMatrixSuite + +# Verify compilation succeeded (indicating no crash during the above command) +> checkEmptyRun \ No newline at end of file diff --git a/src/sbt-test/simple/parameterized-matrix-runner/build.sbt b/src/sbt-test/simple/parameterized-matrix-runner/build.sbt new file mode 100644 index 0000000..c9df2fc --- /dev/null +++ b/src/sbt-test/simple/parameterized-matrix-runner/build.sbt @@ -0,0 +1,26 @@ +name := "parameterized-matrix-runner" + +scalaVersion := "3.3.6" + +libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % Test + +Test / fork := false + +val checkMatrixRun = taskKey[Unit]("Check that the parameterized matrix ran correctly") + +checkMatrixRun := { + // Verify the matrix output file was created with expected content + val matrixOutput = target.value / "matrix-results.txt" + assert(matrixOutput.exists(), "Matrix results file should have been created") + + val results = IO.readLines(matrixOutput).toSet + val expected = Set( + "test-env1-input1", + "test-env1-input2", + "test-env2-input1", + "test-env2-input2" + ) + + assert(results == expected, s"Expected matrix results $expected, but got $results") + streams.value.log.info(s"Parameterized matrix test ran successfully with ${results.size} combinations") +} \ No newline at end of file diff --git a/src/sbt-test/simple/parameterized-matrix-runner/src/test/scala/JSEnvMatrixSuite.scala b/src/sbt-test/simple/parameterized-matrix-runner/src/test/scala/JSEnvMatrixSuite.scala new file mode 100644 index 0000000..0998ab2 --- /dev/null +++ b/src/sbt-test/simple/parameterized-matrix-runner/src/test/scala/JSEnvMatrixSuite.scala @@ -0,0 +1,8 @@ +import org.junit.runner.RunWith + +// A test suite that uses parameterized matrix runner to generate multiple test combinations +@RunWith(classOf[MatrixRunner]) +class JSEnvMatrixSuite { + // The actual test logic is handled by the custom runner + // This class serves as the entry point for test discovery +} diff --git a/src/sbt-test/simple/parameterized-matrix-runner/src/test/scala/MatrixRunner.scala b/src/sbt-test/simple/parameterized-matrix-runner/src/test/scala/MatrixRunner.scala new file mode 100644 index 0000000..86f9059 --- /dev/null +++ b/src/sbt-test/simple/parameterized-matrix-runner/src/test/scala/MatrixRunner.scala @@ -0,0 +1,50 @@ +import org.junit.runner.{Description, Runner} +import org.junit.runner.notification.RunNotifier +import org.junit.Test +import java.io.{File, FileWriter} + +// Simulates a parameterized test runner that dynamically generates test combinations +final class MatrixRunner(testClass: Class[_]) extends Runner { + private val environments = List("env1", "env2") + private val inputs = List("input1", "input2") + + private val root = Description.createSuiteDescription(testClass) + + // Generate matrix of test descriptions + private val testCombinations = for { + env <- environments + input <- inputs + } yield { + val testDesc = Description.createTestDescription(testClass, s"test-$env-$input") + root.addChild(testDesc) + (env, input, testDesc) + } + + override def getDescription(): Description = root + + override def run(notifier: RunNotifier): Unit = { + notifier.fireTestRunStarted(root) + + testCombinations.foreach { case (env, input, desc) => + notifier.fireTestStarted(desc) + try { + // Simulate test execution - write result to file + val outputFile = new File("target/matrix-results.txt") + outputFile.getParentFile.mkdirs() + val writer = new FileWriter(outputFile, true) // append mode + try { + writer.write(s"test-$env-$input\n") + } finally { + writer.close() + } + + notifier.fireTestFinished(desc) + } catch { + case e: Exception => + notifier.fireTestFailure(new org.junit.runner.notification.Failure(desc, e)) + } + } + + notifier.fireTestRunFinished(new org.junit.runner.Result()) + } +} diff --git a/src/sbt-test/simple/parameterized-matrix-runner/test b/src/sbt-test/simple/parameterized-matrix-runner/test new file mode 100644 index 0000000..431e2ec --- /dev/null +++ b/src/sbt-test/simple/parameterized-matrix-runner/test @@ -0,0 +1,13 @@ +# Test a working parameterized matrix case with zero children at discovery time + +# Clean any previous results +$ delete target/matrix-results.txt + +# Compile the test classes +> Test/compile + +# Run the parameterized matrix suite +> testOnly *JSEnvMatrixSuite + +# Verify the matrix executed all combinations correctly +> checkMatrixRun diff --git a/src/sbt-test/simple/test-listener-multiple/build.sbt b/src/sbt-test/simple/test-listener-multiple/build.sbt index 781b1b0..8bca825 100644 --- a/src/sbt-test/simple/test-listener-multiple/build.sbt +++ b/src/sbt-test/simple/test-listener-multiple/build.sbt @@ -1,6 +1,6 @@ name := "test-project" -scalaVersion := "2.10.7" +scalaVersion := "2.11.12" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % Test diff --git a/src/sbt-test/simple/test-listener/build.sbt b/src/sbt-test/simple/test-listener/build.sbt index 5111250..8fdd078 100644 --- a/src/sbt-test/simple/test-listener/build.sbt +++ b/src/sbt-test/simple/test-listener/build.sbt @@ -1,6 +1,6 @@ name := "test-project" -scalaVersion := "2.10.7" +scalaVersion := "2.11.12" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" diff --git a/src/sbt-test/simple/test-report-ignored-tests/build.sbt b/src/sbt-test/simple/test-report-ignored-tests/build.sbt index e7d0178..bc75627 100644 --- a/src/sbt-test/simple/test-report-ignored-tests/build.sbt +++ b/src/sbt-test/simple/test-report-ignored-tests/build.sbt @@ -1,6 +1,6 @@ name := "test-report-ignored-tests" -scalaVersion := "2.13.6" +scalaVersion := "2.13.16" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" diff --git a/src/sbt-test/simple/test-report-ignored-tests/project/build.properties b/src/sbt-test/simple/test-report-ignored-tests/project/build.properties deleted file mode 100644 index 83be3cf..0000000 --- a/src/sbt-test/simple/test-report-ignored-tests/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.5.2 \ No newline at end of file diff --git a/src/sbt-test/simple/test-run-same-test-multiple-times/build.sbt b/src/sbt-test/simple/test-run-same-test-multiple-times/build.sbt index dbe2e1e..390a0a2 100644 --- a/src/sbt-test/simple/test-run-same-test-multiple-times/build.sbt +++ b/src/sbt-test/simple/test-run-same-test-multiple-times/build.sbt @@ -1,6 +1,6 @@ name := "test-report-ignored-tests" -scalaVersion := "2.13.6" +scalaVersion := "2.13.16" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test" diff --git a/src/sbt-test/simple/test-run-same-test-multiple-times/project/build.properties b/src/sbt-test/simple/test-run-same-test-multiple-times/project/build.properties deleted file mode 100644 index 83be3cf..0000000 --- a/src/sbt-test/simple/test-run-same-test-multiple-times/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.5.2 \ No newline at end of file diff --git a/src/sbt-test/simple/tests-run-once/build.sbt b/src/sbt-test/simple/tests-run-once/build.sbt index 9fcd405..83bda4f 100644 --- a/src/sbt-test/simple/tests-run-once/build.sbt +++ b/src/sbt-test/simple/tests-run-once/build.sbt @@ -1,6 +1,6 @@ name := """tests-run-once""" -scalaVersion := "2.10.7" +scalaVersion := "2.11.12" libraryDependencies += "com.github.sbt" % "junit-interface" % sys.props("plugin.version") % "test"