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/08 08:39:23 UTC

(maven-surefire) branch master updated: fix: report AfterAll/AfterClass exceptions as errors again (#3412) (#3413)

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 92e6174fd fix: report AfterAll/AfterClass exceptions as errors again (#3412) (#3413)
92e6174fd is described below

commit 92e6174fd819c28318e93dd19cee75fe411bbc7c
Author: dev_Hakaze <af...@gmail.com>
AuthorDate: Sat Aug 8 15:39:17 2026 +0700

    fix: report AfterAll/AfterClass exceptions as errors again (#3412) (#3413)
    
    Since 3.5.5, class-level teardown failures (@AfterAll / @AfterClass) were
    misclassified as flaky BeforeAll failures whenever any test method in the
    same class had succeeded. That made errors disappear from suite totals,
    turned deterministic teardown failures into flakes, and needlessly re-ran
    already-passing tests when rerunFailingTestsCount was set.
    
    Distinguish setup vs teardown at the JUnit Platform adapter:
    - Before any test method succeeds → initializationError (flaky-BeforeAll path)
    - After a successful test method → executionError (real error, not re-run)
    
    Keep BeforeAll flake special-casing for initializationError only.
    
    Signed-off-by: arimu1 <19...@users.noreply.github.com>
    Co-authored-by: arimu1 <19...@users.noreply.github.com>
---
 .../surefire/report/DefaultReporterFactory.java    |  3 +-
 .../surefire/report/StatelessXmlReporter.java      | 30 ++++++---
 .../report/DefaultReporterFactoryTest.java         | 67 +++++++++++++++++++
 .../surefire/its/JUnit4FailingAfterClassIT.java    | 76 +++++++++++++++++++++
 .../surefire/its/JUnit5FailingAfterAllIT.java      | 78 ++++++++++++++++++++++
 .../resources/junit4-failing-after-class/pom.xml   | 55 +++++++++++++++
 .../java/junit4/AlwaysFailingAfterClassTest.java   | 29 ++++++++
 .../src/test/java/junit4/PassingTest.java          | 16 +++++
 .../resources/junit5-failing-after-all/pom.xml     | 69 +++++++++++++++++++
 .../java/junit5/AlwaysFailingAfterAllTest.java     | 29 ++++++++
 .../src/test/java/junit5/PassingTest.java          | 16 +++++
 .../surefire/junitplatform/RunListenerAdapter.java | 43 ++++++++++--
 .../junitplatform/JUnitPlatformProviderTest.java   | 43 ++++++++++++
 .../junitplatform/RunListenerAdapterTest.java      | 31 +++++++++
 14 files changed, 572 insertions(+), 13 deletions(-)

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactory.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactory.java
index fd386b519..6d6cb92c5 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactory.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactory.java
@@ -276,7 +276,8 @@ private void mergeTestHistoryResult() {
             List<TestMethodStats> testMethodStats = entry.getValue();
             String testClassMethodName = entry.getKey();
 
-            // Handle @BeforeAll failures (null, empty, ends with ".null" or ".initializationError" method names)
+            // Handle @BeforeAll failures (null, empty, ends with ".null" or ".initializationError" method names).
+            // Do NOT include ".executionError" (AfterAll/AfterClass) — those stay as real errors (#3412).
             // But only if they actually failed (ERROR or FAILURE), not if they were skipped
             if ((StringUtils.isBlank(testClassMethodName)
                             || testClassMethodName.endsWith(".null")
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java
index eb7554085..794662260 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java
@@ -393,9 +393,12 @@ private TestResultType getTestResultType(List<WrappedReportEntry> methodEntryLis
 
     /**
      * Determines the final result type for a test method, applying special handling for @BeforeAll failures.
-     * If a @BeforeAll fails but any actual test methods succeed, it's classified as a FLAKE.
+     * If a @BeforeAll fails but any actual test methods succeed (on a later rerun), it's classified as a FLAKE.
+     * <p>
+     * AfterAll/AfterClass failures are reported as {@code executionError} by the JUnit Platform provider and
+     * are intentionally <em>not</em> covered here — they must remain errors (#3412).
      *
-     * @param methodName the name of the test method (null or "null" for @BeforeAll)
+     * @param methodName the name of the test method (null, "null", or "initializationError" for @BeforeAll)
      * @param methodRuns the list of runs for this method
      * @param methodStatistics all method statistics for the test class
      * @return the final TestResultType
@@ -406,14 +409,15 @@ private TestResultType getTestResultTypeWithBeforeAllHandling(
             Map<String, List<WrappedReportEntry>> methodStatistics) {
         TestResultType resultType = getTestResultType(methodRuns);
 
-        // Special handling for @BeforeAll failures (null method name or method name is "null" or "initializationError")
-        // If @BeforeAll failed but any actual test methods succeeded, treat it as a flake
-        if ((methodName == null || methodName.equals("null") || methodName.equals("initializationError"))
+        // Special handling for @BeforeAll failures only (not @AfterAll "executionError" or null-named
+        // teardown failures). If @BeforeAll failed but any actual test methods succeeded on a later
+        // run, treat it as a flake. Do not match methodName == null: AfterAll/AfterClass may also
+        // surface with an empty name and must remain errors (#3412).
+        if (("null".equals(methodName) || "initializationError".equals(methodName))
                 && (resultType == TestResultType.ERROR || resultType == TestResultType.FAILURE)) {
-            // Check if any actual test methods (non-null and not "null" names) succeeded
+            // Check if any actual test methods succeeded (exclude synthetic class-level names)
             boolean hasSuccessfulTestMethods = methodStatistics.entrySet().stream()
-                    .filter(entry ->
-                            entry.getKey() != null && !entry.getKey().equals("null")) // Only actual test methods
+                    .filter(entry -> isActualTestMethodName(entry.getKey()))
                     .anyMatch(entry -> entry.getValue().stream()
                             .anyMatch(reportEntry -> reportEntry.getReportEntryType() == SUCCESS));
 
@@ -425,6 +429,16 @@ private TestResultType getTestResultTypeWithBeforeAllHandling(
         return resultType;
     }
 
+    /**
+     * Whether the method name refers to a real test method rather than a synthetic class-level failure.
+     */
+    private static boolean isActualTestMethodName(String methodName) {
+        return methodName != null
+                && !methodName.equals("null")
+                && !methodName.equals("initializationError")
+                && !methodName.equals("executionError");
+    }
+
     private Deque<WrappedReportEntry> getAddMethodRunHistoryMap(String testClassName) {
         Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get(testClassName);
         if (methodRunHistory == null) {
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactoryTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactoryTest.java
index 4f49c84d6..5af9308ad 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactoryTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/DefaultReporterFactoryTest.java
@@ -216,6 +216,73 @@ public void testMergeTestHistoryResult() throws Exception {
         assertEquals(emptyList(), reporter.getMessages());
     }
 
+    /**
+     * AfterAll/AfterClass failures are reported as {@code Class.executionError}. They must count as
+     * errors (not flakes) even when test methods in the same class succeeded and rerun is enabled
+     * (#3412).
+     */
+    @Test
+    public void testAfterAllFailureRemainsErrorNotFlake() throws Exception {
+        MessageUtils.setColorEnabled(false);
+        File target = new File(System.getProperty("user.dir"), "target");
+        File reportsDirectory = new File(target, "tmp-afterall-3412");
+        StartupReportConfiguration reportConfig = new StartupReportConfiguration(
+                true,
+                true,
+                "PLAIN",
+                false,
+                reportsDirectory,
+                false,
+                null,
+                new File(reportsDirectory, "TESTHASH"),
+                false,
+                2,
+                null,
+                null,
+                false,
+                true,
+                true,
+                false,
+                new SurefireStatelessReporter(),
+                new SurefireConsoleOutputReporter(),
+                new SurefireStatelessTestsetInfoReporter(),
+                new ReporterFactoryOptions());
+
+        DummyTestReporter reporter = new DummyTestReporter();
+        DefaultReporterFactory factory = new DefaultReporterFactory(reportConfig, reporter);
+
+        String afterAllClass = "com.example.AfterAllFailClass";
+        Queue<TestMethodStats> runStats = new ArrayDeque<>();
+        runStats.add(new TestMethodStats(afterAllClass + ".testOne", ReportEntryType.SUCCESS, null));
+        runStats.add(new TestMethodStats(afterAllClass + ".testTwo", ReportEntryType.SUCCESS, null));
+        runStats.add(new TestMethodStats(
+                afterAllClass + ".executionError",
+                ReportEntryType.ERROR,
+                new DummyStackTraceWriter(afterAllClass + ".executionError " + TEST_ERROR_SUFFIX)));
+
+        TestSetRunListener runListener = mock(TestSetRunListener.class);
+        when(runListener.getTestMethodStats()).thenReturn(runStats);
+        factory.addListener(runListener);
+
+        invokeMethod(factory, "mergeTestHistoryResult");
+        RunStatistics mergedStatistics = factory.getGlobalRunStatistics();
+
+        assertEquals(3, mergedStatistics.getCompletedCount());
+        assertEquals(1, mergedStatistics.getErrors());
+        assertEquals(0, mergedStatistics.getFailures());
+        assertEquals(0, mergedStatistics.getFlakes());
+        assertEquals(0, mergedStatistics.getSkipped());
+
+        factory.printTestFailures(TestResultType.ERROR);
+        assertEquals(
+                asList("Errors: ", "  " + afterAllClass + ".executionError " + TEST_ERROR_SUFFIX),
+                reporter.getMessages());
+
+        reporter.reset();
+        factory.printTestFailures(TestResultType.FLAKE);
+        assertEquals(emptyList(), reporter.getMessages());
+    }
+
     static final class DummyTestReporter implements ConsoleLogger {
         private final List<String> messages = new ArrayList<>();
 
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4FailingAfterClassIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4FailingAfterClassIT.java
new file mode 100644
index 000000000..77b25fb7a
--- /dev/null
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4FailingAfterClassIT.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.surefire.its;
+
+import org.apache.maven.shared.verifier.VerificationException;
+import org.apache.maven.surefire.its.fixture.OutputValidator;
+import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Integration test for JUnit 4 {@code @AfterClass} that always fails (#3412).
+ * Teardown failures must be reported as errors (not flakes) and must not
+ * re-execute already-passing test methods when {@code rerunFailingTestsCount} is set.
+ */
+public class JUnit4FailingAfterClassIT extends SurefireJUnit4IntegrationTestCase {
+    private static final String VERSION = "4.13.2";
+
+    @Test
+    public void testAfterClassFailureIsReported() {
+        OutputValidator outputValidator = unpack("junit4-failing-after-class", "-norerun")
+                .setJUnitVersion(VERSION)
+                .maven()
+                .withFailure()
+                .executeTest();
+
+        outputValidator.verifyTextInLog("AfterClass always fails");
+        // 4 completed: PassingTest (1) + AlwaysFailingAfterClassTest (2 methods + executionError)
+        // with 1 error — restore pre-3.5.5 behavior
+        outputValidator.assertTestSuiteResults(4, 1, 0, 0);
+
+        outputValidator
+                .getSurefireReportsXmlFile("TEST-junit4.PassingTest.xml")
+                .assertContainsText("tests=\"1\" errors=\"0\"");
+
+        outputValidator
+                .getSurefireReportsXmlFile("TEST-junit4.AlwaysFailingAfterClassTest.xml")
+                .assertContainsText("errors=\"1\"");
+    }
+
+    @Test
+    public void testAfterClassFailureWithRerunDoesNotRerunPassingTests() throws VerificationException {
+        OutputValidator outputValidator = unpack("junit4-failing-after-class", "-rerun")
+                .setJUnitVersion(VERSION)
+                .maven()
+                .addGoal("-Dsurefire.rerunFailingTestsCount=2")
+                .withFailure()
+                .executeTest();
+
+        outputValidator
+                .getSurefireReportsXmlFile("TEST-junit4.AlwaysFailingAfterClassTest.xml")
+                .assertContainsText("errors=\"1\"");
+
+        // Passing methods must not be re-executed solely because teardown failed
+        outputValidator.assertThatLogLine(containsString("testOne passed"), is(1));
+        outputValidator.assertThatLogLine(containsString("testTwo passed"), is(1));
+    }
+}
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit5FailingAfterAllIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit5FailingAfterAllIT.java
new file mode 100644
index 000000000..75d62dd7f
--- /dev/null
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit5FailingAfterAllIT.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.surefire.its;
+
+import org.apache.maven.shared.verifier.VerificationException;
+import org.apache.maven.surefire.its.fixture.OutputValidator;
+import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Integration test for JUnit 5 {@code @AfterAll} that always fails (#3412).
+ * Teardown failures must be reported as errors (not flakes) and must not
+ * re-execute already-passing test methods when {@code rerunFailingTestsCount} is set.
+ */
+public class JUnit5FailingAfterAllIT extends SurefireJUnit4IntegrationTestCase {
+    private static final String VERSION = "5.9.1";
+
+    @Test
+    public void testAfterAllFailureIsReported() {
+        OutputValidator outputValidator = unpack("junit5-failing-after-all", "-norerun")
+                .setJUnitVersion(VERSION)
+                .maven()
+                .withFailure()
+                .executeTest();
+
+        outputValidator.verifyTextInLog("AfterAll always fails");
+        // 4 completed: PassingTest (1) + AlwaysFailingAfterAllTest (2 methods + executionError)
+        // with 1 error — restore pre-3.5.5 behavior
+        outputValidator.assertTestSuiteResults(4, 1, 0, 0);
+
+        outputValidator
+                .getSurefireReportsXmlFile("TEST-junit5.PassingTest.xml")
+                .assertContainsText("tests=\"1\" errors=\"0\"");
+
+        outputValidator
+                .getSurefireReportsXmlFile("TEST-junit5.AlwaysFailingAfterAllTest.xml")
+                .assertContainsText("name=\"executionError\"")
+                .assertContainsText("errors=\"1\"");
+    }
+
+    @Test
+    public void testAfterAllFailureWithRerunDoesNotRerunPassingTests() throws VerificationException {
+        OutputValidator outputValidator = unpack("junit5-failing-after-all", "-rerun")
+                .setJUnitVersion(VERSION)
+                .maven()
+                .addGoal("-Dsurefire.rerunFailingTestsCount=2")
+                .withFailure()
+                .executeTest();
+
+        outputValidator
+                .getSurefireReportsXmlFile("TEST-junit5.AlwaysFailingAfterAllTest.xml")
+                .assertContainsText("name=\"executionError\"")
+                .assertContainsText("errors=\"1\"");
+
+        // Passing methods must not be re-executed solely because teardown failed
+        outputValidator.assertThatLogLine(containsString("testOne passed"), is(1));
+        outputValidator.assertThatLogLine(containsString("testTwo passed"), is(1));
+    }
+}
diff --git a/surefire-its/src/test/resources/junit4-failing-after-class/pom.xml b/surefire-its/src/test/resources/junit4-failing-after-class/pom.xml
new file mode 100644
index 000000000..9e18e234c
--- /dev/null
+++ b/surefire-its/src/test/resources/junit4-failing-after-class/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.maven.plugins.surefire</groupId>
+    <artifactId>junit4-failing-after-class</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <name>Test for failing @AfterClass in JUnit 4</name>
+
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${surefire.version}</version>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/surefire-its/src/test/resources/junit4-failing-after-class/src/test/java/junit4/AlwaysFailingAfterClassTest.java b/surefire-its/src/test/resources/junit4-failing-after-class/src/test/java/junit4/AlwaysFailingAfterClassTest.java
new file mode 100644
index 000000000..8e87f1967
--- /dev/null
+++ b/surefire-its/src/test/resources/junit4-failing-after-class/src/test/java/junit4/AlwaysFailingAfterClassTest.java
@@ -0,0 +1,29 @@
+package junit4;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+
+/**
+ * Test class with @AfterClass that always fails.
+ * All test methods pass, but the class-level teardown always throws.
+ */
+public class AlwaysFailingAfterClassTest
+{
+    @AfterClass
+    public static void tearDown()
+    {
+        throw new IllegalStateException( "AfterClass always fails" );
+    }
+
+    @Test
+    public void testOne()
+    {
+        System.out.println( "testOne passed" );
+    }
+
+    @Test
+    public void testTwo()
+    {
+        System.out.println( "testTwo passed" );
+    }
+}
diff --git a/surefire-its/src/test/resources/junit4-failing-after-class/src/test/java/junit4/PassingTest.java b/surefire-its/src/test/resources/junit4-failing-after-class/src/test/java/junit4/PassingTest.java
new file mode 100644
index 000000000..d60a7985c
--- /dev/null
+++ b/surefire-its/src/test/resources/junit4-failing-after-class/src/test/java/junit4/PassingTest.java
@@ -0,0 +1,16 @@
+package junit4;
+
+import org.junit.Test;
+
+/**
+ * A simple passing test class to verify that other tests are unaffected
+ * by the failing @AfterClass in another test class.
+ */
+public class PassingTest
+{
+    @Test
+    public void testPassingOne()
+    {
+        System.out.println( "testPassingOne passed" );
+    }
+}
diff --git a/surefire-its/src/test/resources/junit5-failing-after-all/pom.xml b/surefire-its/src/test/resources/junit5-failing-after-all/pom.xml
new file mode 100644
index 000000000..88b0680d4
--- /dev/null
+++ b/surefire-its/src/test/resources/junit5-failing-after-all/pom.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.maven.plugins.surefire</groupId>
+    <artifactId>junit5-failing-after-all</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <name>Test for failing @AfterAll in JUnit 5</name>
+
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${surefire.version}</version>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/surefire-its/src/test/resources/junit5-failing-after-all/src/test/java/junit5/AlwaysFailingAfterAllTest.java b/surefire-its/src/test/resources/junit5-failing-after-all/src/test/java/junit5/AlwaysFailingAfterAllTest.java
new file mode 100644
index 000000000..9180a439c
--- /dev/null
+++ b/surefire-its/src/test/resources/junit5-failing-after-all/src/test/java/junit5/AlwaysFailingAfterAllTest.java
@@ -0,0 +1,29 @@
+package junit5;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class with @AfterAll that always fails.
+ * All test methods pass, but the class-level teardown always throws.
+ */
+public class AlwaysFailingAfterAllTest
+{
+    @AfterAll
+    static void tearDown()
+    {
+        throw new IllegalStateException( "AfterAll always fails" );
+    }
+
+    @Test
+    public void testOne()
+    {
+        System.out.println( "testOne passed" );
+    }
+
+    @Test
+    public void testTwo()
+    {
+        System.out.println( "testTwo passed" );
+    }
+}
diff --git a/surefire-its/src/test/resources/junit5-failing-after-all/src/test/java/junit5/PassingTest.java b/surefire-its/src/test/resources/junit5-failing-after-all/src/test/java/junit5/PassingTest.java
new file mode 100644
index 000000000..f5aa3381b
--- /dev/null
+++ b/surefire-its/src/test/resources/junit5-failing-after-all/src/test/java/junit5/PassingTest.java
@@ -0,0 +1,16 @@
+package junit5;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * A simple passing test class to verify that other tests are unaffected
+ * by the failing @AfterAll in another test class.
+ */
+public class PassingTest
+{
+    @Test
+    public void testPassingOne()
+    {
+        System.out.println( "testPassingOne passed" );
+    }
+}
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 a0c96de46..c7e7c6ebb 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
@@ -23,6 +23,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.stream.Stream;
@@ -58,12 +59,28 @@
  * @since 2.22.0
  */
 final class RunListenerAdapter implements TestExecutionListener, TestOutputReceiver<OutputReportEntry>, RunModeSetter {
+    /**
+     * Report name for class-level failures that happen before any test method runs
+     * (e.g. {@code @BeforeAll} / {@code @BeforeClass}). Eligible for flaky-BeforeAll
+     * special-casing when a later rerun succeeds.
+     */
     private static final String INITIALIZATION_ERROR = "initializationError";
 
+    /**
+     * Report name for class-level failures that happen after at least one test method
+     * already succeeded (e.g. {@code @AfterAll} / {@code @AfterClass}). Must not be
+     * treated as a flaky BeforeAll — these are real errors and must not trigger
+     * needless re-execution of already-passing tests (#3412).
+     */
+    private static final String EXECUTION_ERROR = "executionError";
+
     private final ClassMethodIndexer classMethodIndexer = new ClassMethodIndexer();
     private final ConcurrentMap<TestIdentifier, Long> testStartTime = new ConcurrentHashMap<>();
     private final ConcurrentMap<TestIdentifier, TestExecutionResult> failures = new ConcurrentHashMap<>();
     private final ConcurrentMap<String, TestIdentifier> runningTestIdentifiersByUniqueId = new ConcurrentHashMap<>();
+    /** Class names that had at least one successful test method in the current adapter cycle. */
+    private final Set<String> classesWithSuccessfulTests = ConcurrentHashMap.newKeySet();
+
     private final TestReportListener<TestOutputReportEntry> runListener;
     private final Stoppable stoppable;
     private volatile TestPlan testPlan;
@@ -153,12 +170,21 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult
                         runListener.testSetCompleted(
                                 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
                     }
-                    failures.put(testIdentifier, testExecutionResult);
+                    // Do not record AfterAll/AfterClass container failures for rerun: the test
+                    // methods already passed, and re-executing them cannot fix a deterministic
+                    // teardown failure (#3412).
+                    if (!(isClass && EXECUTION_ERROR.equals(reportEntry.getName()))) {
+                        failures.put(testIdentifier, testExecutionResult);
+                    }
                     fireStopEvent();
                     break;
                 default:
                     if (isTest) {
-                        runListener.testSucceeded(createReportEntry(testIdentifier, null, elapsed));
+                        SimpleReportEntry succeeded = createReportEntry(testIdentifier, null, elapsed);
+                        runListener.testSucceeded(succeeded);
+                        if (succeeded.getSourceName() != null) {
+                            classesWithSuccessfulTests.add(succeeded.getSourceName());
+                        }
                     } else {
                         runListener.testSetCompleted(
                                 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
@@ -244,8 +270,16 @@ private SimpleReportEntry createReportEntry(
                 && testExecutionResult.getStatus() != org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL
                 && testIdentifier.isContainer()
                 && methodName == null) {
-            methodName = INITIALIZATION_ERROR;
-            methodText = INITIALIZATION_ERROR;
+            // BeforeAny tests ran → setup failure; after tests succeeded → teardown failure.
+            // Distinguishing them keeps AfterAll/AfterClass errors from being misclassified as
+            // flaky BeforeAll failures (#3412 / regression since 3.5.5).
+            if (className != null && classesWithSuccessfulTests.contains(className)) {
+                methodName = EXECUTION_ERROR;
+                methodText = EXECUTION_ERROR;
+            } else {
+                methodName = INITIALIZATION_ERROR;
+                methodText = INITIALIZATION_ERROR;
+            }
         }
 
         if (Objects.equals(methodName, methodText)) {
@@ -576,6 +610,7 @@ boolean hasFailingTests() {
     void reset() {
         getFailures().clear();
         testPlan = null;
+        classesWithSuccessfulTests.clear();
     }
 
     @Override
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java
index bd237e945..d01d886af 100644
--- a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java
@@ -46,6 +46,7 @@
 import org.apache.maven.surefire.api.util.RunOrderCalculator;
 import org.apache.maven.surefire.api.util.ScanResult;
 import org.apache.maven.surefire.api.util.TestsToRun;
+import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.DisplayName;
@@ -179,6 +180,37 @@ public void shouldErrorClassOnBeforeAll() throws Exception {
         assertThat(testSetCaptor.getAllValues().get(1).getName()).isNull();
     }
 
+    @Test
+    public void shouldErrorClassOnAfterAll() throws Exception {
+        TestReportListener<TestOutputReportEntry> listener = mock(TestReportListener.class);
+
+        RunListenerAdapter adapter = new RunListenerAdapter(listener, Stoppable.NOOP);
+        adapter.setRunMode(NORMAL_RUN);
+
+        JUnitPlatformProvider provider =
+                new JUnitPlatformProvider(providerParametersMock(), createLauncherSessionWithListeners(adapter));
+
+        ArgumentCaptor<ReportEntry> testCaptor = ArgumentCaptor.forClass(ReportEntry.class);
+        ArgumentCaptor<TestSetReportEntry> testSetCaptor = ArgumentCaptor.forClass(TestSetReportEntry.class);
+
+        TestsToRun testsToRun = newTestsToRun(FailingWithErrorAfterAllJupiterTest.class);
+        invokeProvider(provider, testsToRun);
+        InOrder inOrder = inOrder(listener);
+        inOrder.verify(listener, times(1)).testSetStarting(testSetCaptor.capture());
+        inOrder.verify(listener, times(1)).testStarting(any(ReportEntry.class));
+        inOrder.verify(listener, times(1)).testSucceeded(any(ReportEntry.class));
+        inOrder.verify(listener, times(1)).testError(testCaptor.capture());
+        inOrder.verify(listener, times(1)).testSetCompleted(testSetCaptor.capture());
+
+        assertThat(testCaptor.getValue().getSourceName())
+                .isEqualTo(FailingWithErrorAfterAllJupiterTest.class.getName());
+        // AfterAll must be reported as executionError (not initializationError) so reporters
+        // keep it as a real error instead of a flaky BeforeAll (#3412).
+        assertThat(testCaptor.getValue().getName()).isEqualTo("executionError");
+        // Teardown failures must not be recorded for rerun of already-passing tests
+        assertThat(adapter.hasFailingTests()).isFalse();
+    }
+
     @Test
     public void getSuitesReturnsScannedClasses() {
         ProviderParameters providerParameters = providerParametersMock(TestClass1.class, TestClass2.class);
@@ -1436,6 +1468,17 @@ static void oneTimeSetUp() {
         void test() {}
     }
 
+    static class FailingWithErrorAfterAllJupiterTest {
+
+        @AfterAll
+        static void oneTimeTearDown() {
+            throw new RuntimeException("oneTimeTearDown() threw an exception");
+        }
+
+        @org.junit.jupiter.api.Test
+        void test() {}
+    }
+
     @SuppressWarnings("unchecked")
     private static <T> T getInternalState(Object target, String fieldName) throws Exception {
         Field field = target.getClass().getDeclaredField(fieldName);
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 1c0b8ce7f..77c59456d 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
@@ -463,6 +463,37 @@ public void notifiedWithCorrectNamesWhenClassExecutionErrored() {
         assertThat(entry.getStackTraceWriter().getThrowable().getTarget()).isInstanceOf(RuntimeException.class);
     }
 
+    @Test
+    public void notifiedWithExecutionErrorWhenClassFailsAfterSuccessfulTest() throws Exception {
+        // Simulates @AfterAll: a test method already succeeded, then the class container fails.
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass(ReportEntry.class);
+        EngineDescriptor engineDescriptor = newEngineDescriptor();
+        TestDescriptor classDescriptor = newClassDescriptor();
+        TestDescriptor methodDescriptor = newMethodDescriptor();
+        engineDescriptor.addChild(classDescriptor);
+        classDescriptor.addChild(methodDescriptor);
+
+        TestPlan testPlan = TestPlan.from(false, singleton(engineDescriptor), CONFIG_PARAMS, OUTPUT_DIRECTORY);
+        adapter.testPlanExecutionStarted(testPlan);
+
+        TestIdentifier classIdentifier = TestIdentifier.from(classDescriptor);
+        TestIdentifier methodIdentifier = TestIdentifier.from(methodDescriptor);
+
+        adapter.executionStarted(classIdentifier);
+        adapter.executionStarted(methodIdentifier);
+        adapter.executionFinished(methodIdentifier, successful());
+        adapter.executionFinished(classIdentifier, failed(new RuntimeException("AfterAll always fails")));
+
+        verify(listener).testSucceeded(any());
+        verify(listener).testError(entryCaptor.capture());
+
+        ReportEntry entry = entryCaptor.getValue();
+        assertEquals(MyTestClass.class.getTypeName(), entry.getSourceName());
+        assertEquals("executionError", entry.getName());
+        // AfterAll must not be queued for rerun of already-passing tests (#3412)
+        assertThat(adapter.hasFailingTests()).isFalse();
+    }
+
     @Test
     public void notifiedWithCorrectNamesWhenContainerFailed() throws Exception {
         ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass(ReportEntry.class);