Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ target
.bloop/
.bsp/
.vscode/
metals.sbt
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.5.2
sbt.version = 1.11.6
4 changes: 2 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -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")
60 changes: 53 additions & 7 deletions src/main/java/com/novocode/junit/JUnitTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,17 +58,66 @@ 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);
}
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<Description> children = root.getChildren();
if (children.isEmpty()) return true; // Non-test node, no children → empty
Deque<Description> 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
Expand Down
2 changes: 1 addition & 1 deletion src/sbt-test/simple/can-run-single-test/build.sbt
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
2 changes: 1 addition & 1 deletion src/sbt-test/simple/categories/build.sbt
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
2 changes: 1 addition & 1 deletion src/sbt-test/simple/check-test-selector/build.sbt
Original file line number Diff line number Diff line change
@@ -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

This file was deleted.

24 changes: 24 additions & 0 deletions src/sbt-test/simple/empty-custom-runner/build.sbt
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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 = ()
}
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions src/sbt-test/simple/empty-custom-runner/test
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions src/sbt-test/simple/parameterized-matrix-runner/build.sbt
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
13 changes: 13 additions & 0 deletions src/sbt-test/simple/parameterized-matrix-runner/test
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/sbt-test/simple/test-listener-multiple/build.sbt
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/sbt-test/simple/test-listener/build.sbt
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
2 changes: 1 addition & 1 deletion src/sbt-test/simple/test-report-ignored-tests/build.sbt
Original file line number Diff line number Diff line change
@@ -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"

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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"

Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion src/sbt-test/simple/tests-run-once/build.sbt
Original file line number Diff line number Diff line change
@@ -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"

Expand Down