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
74 changes: 64 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ repositories {
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
}

import org.gradle.api.attributes.Category
import org.gradle.api.attributes.Usage

configurations {
junit5Launcher {
canBeConsumed = false
canBeResolved = true
transitive = true
}
junit6Launcher {
canBeConsumed = false
canBeResolved = true
transitive = true
}
}

dependencies {

implementation group: 'commons-io', name: 'commons-io', version: '2.8.0'
Expand All @@ -46,8 +62,8 @@ dependencies {
// ---- JUnit for tests of Gin ----
testImplementation platform('org.junit:junit-bom:5.11.0')
testImplementation 'org.junit.jupiter:junit-jupiter' // API + parameterized, etc.
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0'
testCompileOnly 'org.apiguardian:apiguardian-api:1.1.2'

// for LLM integrations
Expand Down Expand Up @@ -104,13 +120,15 @@ dependencies {

testImplementation "org.mockito:mockito-core:3.+"

constraints {
implementation('org.junit.platform:junit-platform-commons:1.11.0')
implementation('org.junit.platform:junit-platform-launcher:1.11.0')
implementation('org.junit.platform:junit-platform-engine:1.11.0')
implementation('org.junit.jupiter:junit-jupiter-engine:5.11.0')
implementation('org.junit.vintage:junit-vintage-engine:5.11.0')
}
junit5Launcher 'org.junit.platform:junit-platform-launcher:1.11.0'

junit6Launcher platform("org.junit:junit-bom:6.0.2")
junit6Launcher "org.junit.platform:junit-platform-launcher"
junit6Launcher "org.junit.jupiter:junit-jupiter-engine"
junit6Launcher "org.junit.platform:junit-platform-engine"
junit6Launcher "org.junit.platform:junit-platform-commons"
junit6Launcher "org.opentest4j:opentest4j:1.3.0"
junit6Launcher "org.apiguardian:apiguardian-api:1.1.2"
}

jar {
Expand All @@ -136,10 +154,39 @@ javadoc {
}

shadowJar {
destinationDirectory = buildDir
dependencies {
exclude(dependency("org.junit.platform:.*"))
exclude(dependency("org.junit.jupiter:.*"))
exclude(dependency("org.junit.vintage:.*"))
exclude(dependency("junit:junit"))
}

into("embedded-libs/junit5") {
from {
project.configurations.junit5Launcher
.resolvedConfiguration
.resolvedArtifacts
.findAll { it.moduleVersion.id.group == "org.junit.platform" && it.name == "junit-platform-launcher" }
.collect { it.file }
}
rename { "launcher.jar" }
}
into("embedded-libs/junit6") {
from {
project.configurations.junit6Launcher
.resolvedConfiguration
.resolvedArtifacts
.findAll { it.moduleVersion.id.group == "org.junit.platform" && it.name == "junit-platform-launcher" }
.collect { it.file }
}
rename { "launcher.jar" }
}

destinationDirectory = buildDir
archiveBaseName = 'gin'
archiveVersion = null
archiveClassifier = null
mergeServiceFiles()
}

test {
Expand All @@ -159,3 +206,10 @@ test {
}
}

tasks.test {
dependsOn tasks.shadowJar

// hand the jar path to ExternalTestRunner via system property
systemProperty "gin.jar", "${buildDir}/gin.jar"
}

143 changes: 135 additions & 8 deletions src/main/java/gin/test/ExternalTestRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ private List<UnitTestResult> runTests(int reps) throws IOException, InterruptedE

// Build child classpath: temp dir + project CP (without extra JUnit) + parent CP
String childCp = cleanChildClasspath(this.getClassPath());
if (!cpHasLauncherDiscoveryRequestBuilder(childCp)) {
String bucket = cpHasJunit6(childCp) ? "junit6" : "junit5";
Optional<Path> launcher = extractEmbeddedLauncher(bucket);
if (launcher.isPresent()) {
childCp = childCp + File.pathSeparator + launcher.get().toAbsolutePath();
}
}
String rawClasspath = this.getTemporaryDirectory() + File.pathSeparator +
childCp + File.pathSeparator +
System.getProperty("java.class.path");
Expand Down Expand Up @@ -218,13 +225,20 @@ private List<UnitTestResult> runTests(int reps) throws IOException, InterruptedE
// == Start one harness JVM for THIS MODULE ==
while (index < maxIndex) {

ProcessBuilder builder = new ProcessBuilder(
jvm.getAbsolutePath(),
"-Dtinylog.level=" + Logger.getLevel(),
"-cp", classpath,
HARNESS_CLASS
);
// This alone is enough to make '.' the module directory for file I/O:
String moduleClasspath = withModuleOutputsFirst(classpath, moduleDir);

List<String> cmd = new ArrayList<>();
cmd.add(jvm.getAbsolutePath());
cmd.add("-Dtinylog.level=" + Logger.getLevel());

// add javaagent if present
findSystemExitAgentJar(classpath).ifPresent(jar -> cmd.add("-javaagent:" + jar));

cmd.add("-cp");
cmd.add(moduleClasspath);
cmd.add(HARNESS_CLASS);

ProcessBuilder builder = new ProcessBuilder(cmd);
builder.directory(moduleDir);

final Process process = builder
Expand Down Expand Up @@ -489,7 +503,7 @@ private static boolean isJUnit4OrVintageLibPath(String path) {
String name = new java.io.File(path).getName().toLowerCase(java.util.Locale.ROOT);
// Exclude JUnit 4 and the Vintage engine
if (name.startsWith("junit-vintage-")) return true; // Vintage engine
if (name.matches("^junit-\\d+.*\\.jar$")) return true; // junit-4.x.jar
//if (name.matches("^junit-\\d+.*\\.jar$")) return true; // junit-4.x.jar - later decided to comment this out. the library is needed for legacy builds!

// Also exclude obvious Vintage directories on classpath
String p = path.replace('\\', '/');
Expand All @@ -499,4 +513,117 @@ private static boolean isJUnit4OrVintageLibPath(String path) {
return false;
}

private static Optional<Path> extractEmbeddedLauncher(String bucket) {
String resPath = "/embedded-libs/" + bucket + "/launcher.jar";
try (InputStream in = ExternalTestRunner.class.getResourceAsStream(resPath)) {
if (in == null) return Optional.empty();
Path tmpDir = Files.createTempDirectory("gin-junit-launcher-");
tmpDir.toFile().deleteOnExit();
Path out = tmpDir.resolve("launcher.jar");
try (OutputStream os = Files.newOutputStream(out)) {
in.transferTo(os);
}
out.toFile().deleteOnExit();
return Optional.of(out);
} catch (IOException e) {
Logger.error("Failed to extract embedded JUnit launcher: " + e);
return Optional.empty();
}
}

private static boolean cpHasPlatformLauncher(String cp) {
if (cp == null || cp.isBlank()) return false;
for (String p : cp.split(File.pathSeparator)) {
String name = new File(p).getName().toLowerCase(Locale.ROOT);
if (name.startsWith("junit-platform-launcher-")) return true;
// directory-style classpaths (rare)
String norm = p.replace('\\','/');
if (norm.contains("/org/junit/platform/launcher/")) return true;
}
return false;
}

private static boolean cpHasJunit6(String cp) {
if (cp == null || cp.isBlank()) return false;

for (String p : cp.split(File.pathSeparator)) {
String name = new File(p).getName().toLowerCase(Locale.ROOT);

// Strong signal: Jupiter 6.x present
if (name.startsWith("junit-jupiter-") && name.contains("-6.")) return true;
if (name.startsWith("junit-jupiter-engine-") && name.contains("-6.")) return true;
if (name.startsWith("junit-jupiter-api-") && name.contains("-6.")) return true;

// Optional: also treat JUnit Platform Suite 6.x as “JUnit 6-era”
if (name.startsWith("junit-platform-suite-") && name.contains("-6.")) return true;
}
return false;
}

private static boolean cpHasLauncherDiscoveryRequestBuilder(String cp) {
if (cp == null || cp.isBlank()) return false;

for (String p : cp.split(File.pathSeparator)) {
if (p == null || p.isBlank()) continue;

File f = new File(p);
if (!f.exists()) continue;

try {
if (f.isDirectory()) {
File cls = new File(f, "org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.class");
if (cls.isFile()) return true;
} else if (f.isFile() && f.getName().endsWith(".jar")) {
try (java.util.jar.JarFile jar = new java.util.jar.JarFile(f)) {
if (jar.getEntry("org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.class") != null) {
return true;
}
}
}
} catch (IOException ignored) { }
}

return false;
}

private static Optional<String> findSystemExitAgentJar(String cp) {
if (cp == null) return Optional.empty();
for (String e : cp.split(File.pathSeparator)) {
if (e == null) continue;
String n = new File(e).getName();
if (n.startsWith("junit5-system-exit-") && n.endsWith(".jar")) {
return Optional.of(e);
}
}
return Optional.empty();
}

private static String appendIfDirExists(String cp, File dir) {
if (dir != null && dir.isDirectory()) {
String p = dir.getAbsolutePath();
if (!cp.contains(p)) {
return cp + File.pathSeparator + p;
}
}
return cp;
}

private static String withModuleOutputsFirst(String baseCp, File moduleDir) {
String cp = baseCp;

File testClasses = new File(moduleDir, "target/test-classes");
File mainClasses = new File(moduleDir, "target/classes");

// Prepend (order matters: test-classes before classes)
List<String> parts = new ArrayList<>();
if (testClasses.isDirectory()) parts.add(testClasses.getAbsolutePath());
if (mainClasses.isDirectory()) parts.add(mainClasses.getAbsolutePath());

// Keep the existing cp after
parts.add(cp);

return String.join(File.pathSeparator, parts);
}


}
3 changes: 1 addition & 2 deletions src/main/java/gin/test/JUnitBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public UnitTestResult runTest(UnitTest test, int rep) {
utr.setTimedOut(false);
}

return result;
return utr;
} else {
result.setPassed(false);
result.setExceptionType("gin.test.NoTestsDiscovered");
Expand Down Expand Up @@ -159,7 +159,6 @@ private LauncherDiscoveryRequest buildRequest(UnitTest test)
if (m == null) throw new NoSuchMethodException(className + "#" + method);

return org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request()
.filters(org.junit.platform.launcher.EngineFilter.includeEngines("junit-jupiter","junit-vintage"))
.configurationParameter("junit.jupiter.execution.timeout.test.method.default",
test.getTimeoutMS() + " ms")
.selectors(
Expand Down
Loading
Loading