diff --git a/build.gradle b/build.gradle index 1d563639..4f2d773f 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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 @@ -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 { @@ -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 { @@ -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" +} + diff --git a/src/main/java/gin/test/ExternalTestRunner.java b/src/main/java/gin/test/ExternalTestRunner.java index 5c623215..94c79a79 100644 --- a/src/main/java/gin/test/ExternalTestRunner.java +++ b/src/main/java/gin/test/ExternalTestRunner.java @@ -177,6 +177,13 @@ private List 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 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"); @@ -218,13 +225,20 @@ private List 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 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 @@ -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('\\', '/'); @@ -499,4 +513,117 @@ private static boolean isJUnit4OrVintageLibPath(String path) { return false; } + private static Optional 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 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 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); + } + + } diff --git a/src/main/java/gin/test/JUnitBridge.java b/src/main/java/gin/test/JUnitBridge.java index 470181f6..7b4100c6 100644 --- a/src/main/java/gin/test/JUnitBridge.java +++ b/src/main/java/gin/test/JUnitBridge.java @@ -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"); @@ -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( diff --git a/src/main/java/gin/test/TestHarness.java b/src/main/java/gin/test/TestHarness.java index cc9ad832..1214ea4c 100644 --- a/src/main/java/gin/test/TestHarness.java +++ b/src/main/java/gin/test/TestHarness.java @@ -1,13 +1,13 @@ package gin.test; import com.sampullara.cli.Args; +import java.util.Locale; + import edu.emory.mathcs.backport.java.util.Arrays; -import org.junit.platform.launcher.Launcher; -import org.junit.platform.launcher.LauncherDiscoveryRequest; -import org.junit.platform.launcher.LauncherSession; -import org.junit.platform.launcher.TestPlan; +import gin.util.JavaUtils; +import org.junit.platform.launcher.*; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.launcher.core.LauncherFactory; import org.pmw.tinylog.Logger; @@ -20,11 +20,12 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.*; -import org.junit.platform.launcher.EngineFilter; + import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.Request; import org.junit.runner.notification.Failure; +import org.slf4j.LoggerFactory; /** * Runs a given test request. Uses sockets to communicate with ExternalTestRunner. @@ -34,6 +35,7 @@ public class TestHarness implements Serializable { public static final String PORT_PREFIX = "PORT"; @Serial private static final long serialVersionUID = -6547478455821943382L; + private static final org.slf4j.Logger log = LoggerFactory.getLogger(TestHarness.class); private ServerSocket serverSocket; private Socket clientSocket; private PrintWriter out; @@ -49,6 +51,8 @@ public static void main(String[] args) { } public void start() { + logDebug("TH.start", System.getProperty("java.class.path") + "\n\nuser.dir=" + System.getProperty("user.dir")); + try { serverSocket = new ServerSocket(0); int port = serverSocket.getLocalPort(); @@ -105,6 +109,7 @@ public void stop() { /* this method is for debugging purposes; the testharness's output doesn't * end up in the main log so we write directly to a local file instead */ public static void logDebug(String tag, String text) { + /* try { java.util.List lines = new java.util.ArrayList<>(); @@ -113,6 +118,7 @@ public static void logDebug(String tag, String text) { java.nio.file.Path file = java.nio.file.Paths.get("/home/sbr/gin/DEBUG-"+tag+"."+System.nanoTime()+".txt"); java.nio.file.Files.write(file, lines, java.nio.charset.StandardCharsets.UTF_8); } catch (IOException e) {} + */ } @@ -167,6 +173,8 @@ private String runTest(String command) throws ParseException { private UnitTestResult runTest(UnitTest test, int rep) { + logDebug("TH.runTest", System.getProperty("java.class.path")); + UnitTestResult result = new UnitTestResult(test, rep); final String className = test.getFullClassName(); @@ -176,6 +184,9 @@ private UnitTestResult runTest(UnitTest test, int rep) { Class clazz = Class.forName(className); boolean jupiterish = looksLikeJupiter(clazz); + boolean hasJupiter = hasEngine("junit-jupiter"); + boolean hasVintage = hasEngine("junit-vintage"); + // 1) Try method+class discovery with the right engine bias LauncherDiscoveryRequest baseReq = buildRequest(test, jupiterish); @@ -185,14 +196,23 @@ private UnitTestResult runTest(UnitTest test, int rep) { TestPlan plan = launcher.discover(baseReq); boolean hasTests = plan.containsTests(); + logDebug("TH.launcher", "DISCOVERY -> engines=" + launcher.discover(baseReq).countTestIdentifiers(TestIdentifier::isTest) + + ", roots=" + launcher.discover(baseReq).getRoots().size() + ", engines present:" + java.util.ServiceLoader.load(org.junit.platform.engine.TestEngine.class).stream().map(p->p.type().getName()).toList()); + // 2) If nothing found, try class-only discovery if (!hasTests) { - var classOnly = LauncherDiscoveryRequestBuilder.request() - .selectors(selectClass(clazz)) - .filters(EngineFilter.includeEngines( - jupiterish ? new String[]{"junit-jupiter"} - : new String[]{"junit-jupiter", "junit-vintage"})) - .build(); + var classOnlyBuilder = LauncherDiscoveryRequestBuilder.request() + .selectors(selectClass(clazz)); + if (jupiterish) { + if (hasJupiter) classOnlyBuilder.filters(EngineFilter.includeEngines("junit-jupiter")); + } else { + if (hasJupiter && hasVintage) classOnlyBuilder.filters(EngineFilter.includeEngines("junit-jupiter", "junit-vintage")); + else if (hasJupiter) classOnlyBuilder.filters(EngineFilter.includeEngines("junit-jupiter")); + else if (hasVintage) classOnlyBuilder.filters(EngineFilter.includeEngines("junit-vintage")); + // else: no filter (let launcher decide) + } + + var classOnly = classOnlyBuilder.build(); TestPlan plan2 = launcher.discover(classOnly); if (plan2.containsTests()) { @@ -201,19 +221,89 @@ private UnitTestResult runTest(UnitTest test, int rep) { } } + logDebug("TH.legacy", + "class=" + clazz.getName() + + " method=" + methodName + + " jupiterish=" + jupiterish + + " hasTests=" + hasTests + + " hasJUnit4Annotations=" + hasJUnit4Annotations(clazz, methodName) + + " superclass=" + (clazz.getSuperclass() == null ? "null" : clazz.getSuperclass().getName())); + // 3) If still nothing, consider JUnit 4 fallback only when appropriate - if (!hasTests && !jupiterish && hasJUnit4Annotations(clazz, methodName)) { - Request req = Request.method(clazz, methodName); - Result r = new JUnitCore().run(req); - - UnitTestResult utr = new UnitTestResult(test, rep); - utr.setPassed(r.wasSuccessful()); - for (Failure f : r.getFailures()) { - utr.addFailure(new Failure( - org.junit.runner.Description.createTestDescription(clazz, methodName), - f.getException())); + for (Method mm : clazz.getDeclaredMethods()) { + logDebug("TH.methods", + clazz.getName() + " :: " + mm.getName() + + " annotations=" + java.util.Arrays.toString(mm.getAnnotations())); + } + if (!hasTests && !jupiterish) { // && hasJUnit4Annotations(clazz, methodName)) { + // First try the specific method + try { + logDebug("TH.junitcore", "Trying JUnitCore method request for " + clazz.getName() + "#" + methodName); + + Request req = Request.method(clazz, methodName); + Result r = new JUnitCore().run(req); + + UnitTestResult utr = new UnitTestResult(test, rep); + + // If JUnit actually ran something, trust the result + if (r.getRunCount() > 0 || !r.getFailures().isEmpty()) { + utr.setPassed(r.wasSuccessful()); + + for (Failure f : r.getFailures()) { + utr.addFailure(new Failure( + org.junit.runner.Description.createTestDescription(clazz, methodName), + f.getException())); + } + + return utr; + } + } catch (Throwable ignored) { + logDebug("TH.junitcore", "Method request failed for " + clazz.getName() + "#" + methodName + " : " + ignored); + } + + // Second try: run the whole class and infer target result + try { + logDebug("TH.junitcore", "Trying JUnitCore class request for " + clazz.getName()); + + Request req = Request.aClass(clazz); + Result r = new JUnitCore().run(req); + + UnitTestResult utr = new UnitTestResult(test, rep); + + boolean targetFailed = false; + Throwable targetThrowable = null; + + for (Failure f : r.getFailures()) { + String failedMethod = null; + if (f.getDescription() != null) { + failedMethod = f.getDescription().getMethodName(); + } + + if (failedMethod != null && normalizeMethodName(failedMethod).equals(methodName)) { + targetFailed = true; + targetThrowable = f.getException(); + break; + } + } + + if (targetFailed) { + utr.setPassed(false); + if (targetThrowable != null) { + utr.setExceptionType(targetThrowable.getClass().getName()); + utr.setExceptionMessage(String.valueOf(targetThrowable.getMessage())); + } + return utr; + } + + // If the class ran and the target method did not fail, treat it as passed + if (r.getRunCount() > 0) { + utr.setPassed(true); + return utr; + } + + } catch (Throwable ignored) { + logDebug("TH.junitcore", "Class request failed for " + clazz.getName() + " : " + ignored); } - return utr; } // 4) If still nothing, report @@ -221,7 +311,7 @@ private UnitTestResult runTest(UnitTest test, int rep) { result.setPassed(false); result.setExceptionType("gin.test.NoTestsDiscovered"); result.setExceptionMessage("No tests discovered for " + - className + "#" + methodName + " (Jupiterish=" + jupiterish + ")"); + className + "#" + methodName + " (Jupiterish=" + jupiterish + " hasJupiter=" + hasJupiter + " hasVintage=" + hasVintage + ")"); return result; } @@ -232,9 +322,30 @@ private UnitTestResult runTest(UnitTest test, int rep) { } } catch (Throwable t) { + logDebug("TH.caught", t.toString()); + result.setPassed(false); + + // unwrap & stringify cause chain + suppressed + top stack frame + StringBuilder sb = new StringBuilder(512); + Throwable cur = t; + int depth = 0; + while (cur != null && depth < 6) { + sb.append(cur.getClass().getName()).append(": ") + .append(cur.getMessage() == null ? "" : cur.getMessage()).append(" | "); + for (Throwable sup : cur.getSuppressed()) { + sb.append("[suppressed: ").append(sup.getClass().getName()) + .append(": ").append(String.valueOf(sup.getMessage())).append("] "); + } + cur = cur.getCause(); + depth++; + } + + sb.append("; child classpath="); + sb.append(System.getProperty("java.class.path")); + result.setExceptionType(t.getClass().getName()); - result.setExceptionMessage(t.getMessage()); + result.setExceptionMessage(sb.toString()); return result; } } @@ -248,13 +359,24 @@ private LauncherDiscoveryRequest buildRequest(UnitTest test, boolean jupiterish) String method = normalizeMethodName(test.getMethodName()); var builder = LauncherDiscoveryRequestBuilder.request() - .filters(EngineFilter.includeEngines( - jupiterish ? new String[]{"junit-jupiter"} - : new String[]{"junit-jupiter", "junit-vintage"})) // Jupiter timeout only; harmless for Vintage .configurationParameter("junit.jupiter.execution.timeout.test.method.default", test.getTimeoutMS() + " ms"); + // Change C: only filter to engines that actually exist on this classpath + boolean hasJupiter = hasEngine("junit-jupiter"); + boolean hasVintage = hasEngine("junit-vintage"); + + if (jupiterish) { + if (hasJupiter) builder.filters(EngineFilter.includeEngines("junit-jupiter")); + } else { + if (hasJupiter && hasVintage) builder.filters(EngineFilter.includeEngines("junit-jupiter", "junit-vintage")); + else if (hasJupiter) builder.filters(EngineFilter.includeEngines("junit-jupiter")); + else if (hasVintage) builder.filters(EngineFilter.includeEngines("junit-vintage")); + // else: no filter (let launcher decide) + } + + // Prefer method-level selector when resolvable; add class as a safety net Method m = findMethodDeep(clazz, method); if (m != null) { @@ -391,4 +513,6 @@ private static boolean hasEngine(String id) { } + + } diff --git a/src/main/java/gin/test/TestRunListener.java b/src/main/java/gin/test/TestRunListener.java index 5971639a..ca74f39f 100644 --- a/src/main/java/gin/test/TestRunListener.java +++ b/src/main/java/gin/test/TestRunListener.java @@ -4,8 +4,6 @@ import org.junit.platform.engine.support.descriptor.MethodSource; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; -import org.junit.runner.Description; -import org.junit.runner.notification.Failure; import org.pmw.tinylog.Logger; import java.io.Serial; @@ -14,7 +12,6 @@ import java.lang.management.ThreadMXBean; import java.util.concurrent.TimeoutException; import org.opentest4j.TestAbortedException; -import org.junit.AssumptionViolatedException; /** @@ -44,6 +41,8 @@ public TestRunListener(UnitTestResult unitTestResult, String cls, String m) { @Override public void executionFinished(TestIdentifier id, TestExecutionResult res) { + Logger.debug("FINISHED id=" + id.getDisplayName() + " uid=" + id.getUniqueId() + " src=" + id.getSource().orElse(null)); + if (!id.isTest()) return; if (!isTarget(id)) return; // only record the target test method @@ -83,8 +82,7 @@ public void executionFinished(TestIdentifier id, TestExecutionResult res) { unitTestResult.setExceptionMessage(String.valueOf(t.getMessage())); break; } - if (t instanceof org.opentest4j.TestAbortedException - || t instanceof org.junit.AssumptionViolatedException) { + if (isAssumption(t)) { unitTestResult.setPassed(true); unitTestResult.setExceptionType(t.getClass().getName()); unitTestResult.setExceptionMessage(String.valueOf(t.getMessage())); @@ -123,28 +121,43 @@ private static String normalize(String m) { private boolean isTarget(TestIdentifier id) { if (!id.isTest()) return false; - // Prefer exact MethodSource match (Jupiter normal path) - return id.getSource() - .filter(s -> s instanceof org.junit.platform.engine.support.descriptor.MethodSource) - .map(s -> (org.junit.platform.engine.support.descriptor.MethodSource) s) - .map(ms -> - ms.getClassName().equals(targetClass) - && normalize(ms.getMethodName()).equals(targetMethod)) - .orElseGet(() -> { - // Fallbacks for engines that don't expose MethodSource (or alter display names) - String dn = id.getDisplayName(); - if (dn == null) return false; - - String n = normalize(dn); - if (n.equals(targetMethod)) return true; // pure method name - if (dn.endsWith("#" + targetMethod)) return true; // Class#method - if (dn.contains("(" + targetMethod + ")")) return true; // parameterized display - // Last resort: if only one test is discovered/executed, accept it. + String wantMethod = normalize(targetMethod); + + // 1) Best: MethodSource (normal Jupiter case) + boolean byMethodSource = id.getSource() + .filter(s -> s instanceof MethodSource) + .map(s -> (MethodSource) s) + .map(ms -> ms.getClassName().equals(targetClass) + && normalize(ms.getMethodName()).equals(wantMethod)) + .orElse(false); + if (byMethodSource) return true; + + // 2) Fallback: UniqueId usually contains the method segment + String uid = id.getUniqueId(); + if (uid != null) { + // match [method:reset()] or [method:reset] + if (uid.contains("[method:" + wantMethod + "]") || uid.contains("[method:" + wantMethod + "(")) { + // also ensure class appears somewhere in uid to avoid collisions + if (uid.contains("[class:" + targetClass + "]") || uid.contains(targetClass)) { return true; - }); + } + } + } + + // 3) Display-name fallbacks + String dn = id.getDisplayName(); + if (dn != null) { + String n = normalize(dn); + if (wantMethod.equals(n)) return true; + if (dn.endsWith("#" + wantMethod)) return true; + if (dn.contains(wantMethod + "(")) return true; // e.g. reset() + } + + return false; } + private static void markSkipped(UnitTestResult result, Throwable t, String msg) { // Ideally we'll add a 'skipped' flag to UnitTestResult // Uncomment this when such a thing exists @@ -207,12 +220,23 @@ public void executionSkipped(TestIdentifier id, String reason) { } public void executionStarted(TestIdentifier testIdentifier) { - if (testIdentifier.isTest()) { - Logger.debug("Test " + testIdentifier.getDisplayName() + " started."); - this.startTime = System.nanoTime(); - this.startCPUTime = threadMXBean.getCurrentThreadCpuTime(); - Runtime runtime = Runtime.getRuntime(); - this.startMemoryUsage = (runtime.totalMemory() - runtime.freeMemory()) / MB; - } + if (!testIdentifier.isTest()) return; + if (!isTarget(testIdentifier)) return; + + Logger.debug("Test " + testIdentifier.getDisplayName() + " started."); + this.startTime = System.nanoTime(); + this.startCPUTime = threadMXBean.getCurrentThreadCpuTime(); + Runtime runtime = Runtime.getRuntime(); + this.startMemoryUsage = (runtime.totalMemory() - runtime.freeMemory()) / MB; } + + private static boolean isAssumption(Throwable t) { + if (t == null) return false; + if (t instanceof org.opentest4j.TestAbortedException) return true; + + // JUnit 4 assumption class name check (doesn't require junit on classpath) + return "org.junit.AssumptionViolatedException".equals(t.getClass().getName()); + } + + } diff --git a/src/main/java/gin/util/JavaUtils.java b/src/main/java/gin/util/JavaUtils.java index 2b35c63a..7505d489 100644 --- a/src/main/java/gin/util/JavaUtils.java +++ b/src/main/java/gin/util/JavaUtils.java @@ -58,13 +58,19 @@ public static String normalizeAndDedupeClasspath(String cp) { } public static String getGinLocation() { + String forced = System.getProperty("gin.jar"); + if (forced != null && new java.io.File(forced).isFile()) return forced; + + java.io.File candidate = new java.io.File("build/gin.jar"); + if (candidate.isFile()) return candidate.getAbsolutePath(); + try { URL loc = JavaUtils.class.getProtectionDomain().getCodeSource().getLocation(); if (loc == null) return ""; Path p = Paths.get(loc.toURI()); return p.toAbsolutePath().normalize().toString(); } catch (Exception e) { - return ""; + return "build/gin.jar"; } } } diff --git a/src/main/java/gin/util/Profiler.java b/src/main/java/gin/util/Profiler.java index 9589eee8..2ebde39a 100644 --- a/src/main/java/gin/util/Profiler.java +++ b/src/main/java/gin/util/Profiler.java @@ -312,8 +312,9 @@ protected List parseTraces(/*Set tests*/ Collection moduleDirs = new LinkedList<>(); @@ -950,6 +950,7 @@ public void runUnitTestMaven(UnitTest test, String args, String taskName, String if (!test.getModuleName().isEmpty()) { request.setProjects(java.util.List.of(test.getModuleName())); // -pl :module + //request.setAlsoMake(true); // Do not add -am here; prime deps in a separate install step if needed } diff --git a/src/main/java/gin/util/Sampler.java b/src/main/java/gin/util/Sampler.java index dd24abe2..1997aff5 100644 --- a/src/main/java/gin/util/Sampler.java +++ b/src/main/java/gin/util/Sampler.java @@ -218,9 +218,16 @@ protected UnitTestResultSet testEmptyPatch(String targetClass, Collection -1 diff --git a/src/main/java/gin/util/Trace.java b/src/main/java/gin/util/Trace.java index 2461da23..1d123c51 100644 --- a/src/main/java/gin/util/Trace.java +++ b/src/main/java/gin/util/Trace.java @@ -184,7 +184,7 @@ private static Map parseHPROFMethodCounts(String hprof, Map parseJFRMethodCounts(File jfrF, Project project) throws IOException { - +int total = 0, exec = 0, st = 0, sts = 0; Map samples = new HashMap<>(); //use main classes to find methods in the main program @@ -193,19 +193,29 @@ private static Map parseJFRMethodCounts(File jfrF, Project proj try (RecordingFile jfr = new RecordingFile(Paths.get(jfrF.getAbsolutePath()))) { //read all events from the JFR profiling file - while (jfr.hasMoreEvents()) { + jfrloop: + while (true) { try { + if (!jfr.hasMoreEvents()) { + break jfrloop; + } + total++; + RecordedEvent event = jfr.readEvent(); String check = event.getEventType().getName(); //if this event is an execution sample, it will contain a call stack snapshot if (check.endsWith("jdk.ExecutionSample")) { // com.oracle.jdk.ExecutionSample for Oracle JDK, jdk.ExecutionSample for OpenJDK + exec++; RecordedStackTrace s = event.getStackTrace(); +// Logger.info("Found sample: " + check); + if (s != null) { //traverse the call stack, if a frame is part of the main program, //return it + Logger.info("Parsing trace..."); for (int i = 0; i < s.getFrames().size(); i++) { RecordedFrame topFrame = s.getFrames().get(i); @@ -217,11 +227,14 @@ private static Map parseJFRMethodCounts(File jfrF, Project proj if (mainClasses.contains(methodName) || mainClasses.contains(className)) { methodName += "." + method.getName() + ":" + topFrame.getLineNumber(); samples.merge(methodName, 1, Integer::sum); + Logger.debug("Found a match"); break; } } + Logger.info("Parsing done."); - + } else { + st++; } } } catch (IOException e) { @@ -230,10 +243,12 @@ private static Map parseJFRMethodCounts(File jfrF, Project proj Logger.warn("IOEx. reading JFR. " + "Probably this is because of something causing multiple writes to the JFR log files." + "If you get lots of these it will likely impact on the reliability of the profiling results."); - //Logger.warn(e); return samples; } } + + Logger.info("Read " + jfrF + ", found " + total + " total events, " + exec + " execs. " + st + " were missing stacktraces."); + return samples; }