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) {