You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by ol...@apache.org on 2026/08/06 11:21:44 UTC

(maven-surefire) branch master updated: Fixes #3264 : tests inside @Suite incorrectly classified as flakes (#3342)

This is an automated email from the ASF dual-hosted git repository.

olamy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git


The following commit(s) were added to refs/heads/master by this push:
     new ce23cec55 Fixes #3264 : tests inside @Suite incorrectly classified as flakes (#3342)
ce23cec55 is described below

commit ce23cec559c2383e9b89face8124fe006350ba0c
Author: Mirko Alicastro <mi...@gmail.com>
AuthorDate: Thu Aug 6 13:21:38 2026 +0200

    Fixes #3264 : tests inside @Suite incorrectly classified as flakes (#3342)
    
    * Fixes #3264 : tests inside @Suite incorrectly classified as flakes
    
    * Attributes suite tests to enclosing class when nested engines expose no test class
---
 .../surefire/its/jiras/Surefire1787JUnit45IT.java  |   2 +-
 .../surefire/junitplatform/RunListenerAdapter.java |  29 ++++-
 .../junitplatform/RunListenerAdapterTest.java      | 119 +++++++++++++++++++++
 3 files changed, 148 insertions(+), 2 deletions(-)

diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1787JUnit45IT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1787JUnit45IT.java
index 89b139857..ce96fbe55 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1787JUnit45IT.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1787JUnit45IT.java
@@ -143,7 +143,7 @@ public void junit5Suite() throws Exception {
                 .verifyTextInLog("Running pkg.domain.AxTest")
                 .assertThatLogLine(containsString("Running pkg.domain.BxTest"), equalTo(0));
 
-        TestFile xmlReportFile = outputValidator.getSurefireReportsXmlFile("TEST-pkg.JUnit5Tests.xml");
+        TestFile xmlReportFile = outputValidator.getSurefireReportsXmlFile("TEST-pkg.domain.AxTest.xml");
         xmlReportFile.assertFileExists();
 
         Source source = Input.fromFile(xmlReportFile.getFile()).build();
diff --git a/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/RunListenerAdapter.java b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/RunListenerAdapter.java
index 881a4a860..a0c96de46 100644
--- a/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/RunListenerAdapter.java
+++ b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/RunListenerAdapter.java
@@ -313,7 +313,34 @@ private TestIdentifier findTopParent(TestIdentifier testIdentifier) {
                 // use deprecated method
                 testPlan.getTestIdentifier(
                         testIdentifier.getParentIdObject().get().toString());
-        return !parent.getParentIdObject().isPresent() ? testIdentifier : findTopParent(parent);
+        if (!parent.getParentIdObject().isPresent()) {
+            return testIdentifier;
+        }
+        // Inside a Suite the hierarchy contains a nested engine (like junit-jupiter under
+        // junit-platform-suite). Stop at that boundary so the test is attributed to its real test
+        // class rather than the Suite class. The ClassSource guard keeps traversing up for engines
+        // that expose no test class below them (like Cucumber features/scenarios), so those tests
+        // fall back to the enclosing Suite class instead of being dropped.
+        if (isEngineIdentifier(parent) && hasClassSource(testIdentifier)) {
+            return testIdentifier;
+        }
+        return findTopParent(parent);
+    }
+
+    private static boolean hasClassSource(TestIdentifier testIdentifier) {
+        return testIdentifier.getSource().filter(ClassSource.class::isInstance).isPresent();
+    }
+
+    private static boolean isEngineIdentifier(TestIdentifier testIdentifier) {
+        String uniqueId = testIdentifier.getUniqueId();
+        int lastOpen = uniqueId.lastIndexOf('[');
+        if (lastOpen >= 0) {
+            int colon = uniqueId.indexOf(':', lastOpen);
+            if (colon > lastOpen) {
+                return "engine".equals(uniqueId.substring(lastOpen + 1, colon));
+            }
+        }
+        return false;
     }
 
     /**
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTest.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTest.java
index 9e5c2fb35..1c0b8ce7f 100644
--- a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTest.java
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTest.java
@@ -602,6 +602,123 @@ public void displayNamesIgnoredInReport() throws NoSuchMethodException {
         assertEquals("some display name", value.getNameText());
     }
 
+    @Test
+    public void notifiedWithActualTestClassNameWhenRunInsideSuite() throws Exception {
+        // Build a Suite hierarchy: SuiteEngine -> SuiteClass -> JupiterEngine -> TestClass -> method
+        // This simulates @Suite @SelectPackages("...") running tests from another class.
+        EngineDescriptor suiteEngine =
+                new EngineDescriptor(UniqueId.forEngine("junit-platform-suite"), "JUnit Platform Suite");
+
+        TestDescriptor suiteClass = new ClassTestDescriptor(
+                suiteEngine.getUniqueId().append("suite", MySuiteClass.class.getName()),
+                MySuiteClass.class,
+                new DefaultJupiterConfiguration(CONFIG_PARAMS, OUTPUT_DIRECTORY));
+        suiteEngine.addChild(suiteClass);
+
+        TestDescriptor jupiterEngine =
+                new AbstractTestDescriptor(
+                        suiteClass.getUniqueId().append("engine", "junit-jupiter"), "JUnit Jupiter") {
+                    @Override
+                    public Type getType() {
+                        return Type.CONTAINER;
+                    }
+                };
+        suiteClass.addChild(jupiterEngine);
+
+        TestDescriptor testClass = new ClassTestDescriptor(
+                jupiterEngine.getUniqueId().append("class", MyTestClass.class.getName()),
+                MyTestClass.class,
+                new DefaultJupiterConfiguration(CONFIG_PARAMS, OUTPUT_DIRECTORY));
+        jupiterEngine.addChild(testClass);
+
+        TestDescriptor method = new TestMethodTestDescriptor(
+                testClass.getUniqueId().append("method", MY_TEST_METHOD_NAME),
+                MyTestClass.class,
+                MyTestClass.class.getDeclaredMethod(MY_TEST_METHOD_NAME),
+                Collections::emptyList,
+                new DefaultJupiterConfiguration(CONFIG_PARAMS, OUTPUT_DIRECTORY));
+        testClass.addChild(method);
+
+        TestPlan plan = TestPlan.from(false, singletonList(suiteEngine), CONFIG_PARAMS, OUTPUT_DIRECTORY);
+        adapter.testPlanExecutionStarted(plan);
+
+        adapter.executionStarted(TestIdentifier.from(suiteEngine));
+        adapter.executionStarted(TestIdentifier.from(suiteClass));
+        adapter.executionStarted(TestIdentifier.from(jupiterEngine));
+        adapter.executionStarted(TestIdentifier.from(testClass));
+        adapter.executionStarted(TestIdentifier.from(method));
+
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass(ReportEntry.class);
+        adapter.executionFinished(TestIdentifier.from(method), failed(new AssertionError("fail")));
+        verify(listener).testFailed(entryCaptor.capture());
+
+        ReportEntry entry = entryCaptor.getValue();
+        // The source name must be the actual test class, not the Suite class
+        assertEquals(MyTestClass.class.getName(), entry.getSourceName());
+        assertEquals(MY_TEST_METHOD_NAME, entry.getName());
+    }
+
+    @Test
+    public void notifiedWithSuiteClassNameWhenEngineHasNoTestClass() {
+        // Build a Suite hierarchy whose nested engine exposes no test class right below it, as
+        // Cucumber does: SuiteEngine -> SuiteClass -> CucumberEngine -> feature -> scenario.
+        // The feature/scenario nodes carry no ClassSource, so the test must be attributed to the
+        // enclosing Suite class instead of being dropped (see issue #3264 follow-up / Cucumber IT).
+        EngineDescriptor suiteEngine =
+                new EngineDescriptor(UniqueId.forEngine("junit-platform-suite"), "JUnit Platform Suite");
+
+        TestDescriptor suiteClass = new ClassTestDescriptor(
+                suiteEngine.getUniqueId().append("suite", MySuiteClass.class.getName()),
+                MySuiteClass.class,
+                new DefaultJupiterConfiguration(CONFIG_PARAMS, OUTPUT_DIRECTORY));
+        suiteEngine.addChild(suiteClass);
+
+        TestDescriptor cucumberEngine =
+                new AbstractTestDescriptor(suiteClass.getUniqueId().append("engine", "cucumber"), "Cucumber") {
+                    @Override
+                    public Type getType() {
+                        return Type.CONTAINER;
+                    }
+                };
+        suiteClass.addChild(cucumberEngine);
+
+        TestDescriptor feature =
+                new AbstractTestDescriptor(cucumberEngine.getUniqueId().append("feature", "sum.feature"), "Sum test") {
+                    @Override
+                    public Type getType() {
+                        return Type.CONTAINER;
+                    }
+                };
+        cucumberEngine.addChild(feature);
+
+        TestDescriptor scenario =
+                new AbstractTestDescriptor(feature.getUniqueId().append("scenario", "1"), "Invalid test") {
+                    @Override
+                    public Type getType() {
+                        return Type.TEST;
+                    }
+                };
+        feature.addChild(scenario);
+
+        TestPlan plan = TestPlan.from(false, singletonList(suiteEngine), CONFIG_PARAMS, OUTPUT_DIRECTORY);
+        adapter.testPlanExecutionStarted(plan);
+
+        adapter.executionStarted(TestIdentifier.from(suiteEngine));
+        adapter.executionStarted(TestIdentifier.from(suiteClass));
+        adapter.executionStarted(TestIdentifier.from(cucumberEngine));
+        adapter.executionStarted(TestIdentifier.from(feature));
+        adapter.executionStarted(TestIdentifier.from(scenario));
+
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass(ReportEntry.class);
+        adapter.executionFinished(TestIdentifier.from(scenario), failed(new AssertionError("fail")));
+        verify(listener).testFailed(entryCaptor.capture());
+
+        ReportEntry entry = entryCaptor.getValue();
+        // No test class exists below the Cucumber engine, so the Suite class is the attribution.
+        assertEquals(MySuiteClass.class.getName(), entry.getSourceName());
+        assertEquals("Invalid test", entry.getName());
+    }
+
     private static TestIdentifier newMethodIdentifier() throws Exception {
         return TestIdentifier.from(newMethodDescriptor());
     }
@@ -720,6 +837,8 @@ void myTestMethod(String foo) {}
         void myNamedTestMethod() {}
     }
 
+    private static class MySuiteClass {}
+
     static class TestMethodTestDescriptorWithDisplayName extends AbstractTestDescriptor {
         private TestMethodTestDescriptorWithDisplayName(
                 UniqueId uniqueId, Class<?> testClass, Method testMethod, String displayName) {