You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by ti...@apache.org on 2023/02/26 16:59:41 UTC

[maven-surefire] 01/01: Test Indexes and Reporters

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

tibordigana pushed a commit to branch test-indexes-and-reporters
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git

commit 8b7d32d4f872e48aaa6f32db2245debfc016ff95
Author: tibordigana <ti...@apache.org>
AuthorDate: Sun Feb 26 17:59:29 2023 +0100

    Test Indexes and Reporters
---
 .../surefire/report/DefaultReporterFactory.java    |   2 +
 .../plugin/surefire/report/ReportEntryType.java    |  20 ++-
 .../surefire/report/ReportersAggregator.java       | 135 +++++++++++++++++++++
 .../surefire/report/StatelessXmlReporter.java      |  61 ++++++----
 .../plugin/surefire/report/TestMethodCalls.java    |  40 ++++++
 .../plugin/surefire/report/TestSetRunListener.java |  70 +++++++----
 .../plugin/surefire/report/TestStatsProcessor.java |   5 +
 .../plugin/surefire/report/WrappedReportEntry.java |   6 +
 .../maven/surefire/api/report/ReportEntry.java     |  21 ++++
 .../surefire/api/report/TestOutputReportEntry.java |  13 ++
 .../apache/maven/surefire/api/report/UniqueID.java |  70 +++++++++++
 .../api/util/internal/ReportEntryUtils.java        |  79 ++++++++++++
 .../maven/surefire/extensions/ReportData.java      |  96 +++++++++++++++
 .../extensions/StatelessReportEventListener.java   |  12 +-
 .../StatelessTestSetSummaryListener.java           |   6 +
 .../TestAssumptionFailureOperation.java            |  22 ++++
 .../testoperations/TestErrorOperation.java         |  22 ++++
 .../TestExecutionSkippedByUserOperation.java       |  22 ++++
 .../testoperations/TestFailedOperation.java        |  22 ++++
 .../extensions/testoperations/TestOperation.java   |  15 +++
 .../testoperations/TestSetCompletedOperation.java  |  22 ++++
 .../testoperations/TestSetStartingOperation.java   |  22 ++++
 .../testoperations/TestSkippedOperation.java       |  22 ++++
 .../testoperations/TestStartingOperation.java      |  22 ++++
 .../testoperations/TestSucceededOperation.java     |  22 ++++
 .../maven/surefire/report/ClassMethodIndexer.java  |  11 +-
 .../junitplatform/JUnitPlatformProvider.java       |   9 +-
 .../surefire/junitplatform/RunListenerAdapter.java |   8 +-
 28 files changed, 804 insertions(+), 73 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 d45ae83cf..7a8a475fb 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
@@ -291,6 +291,8 @@ public class DefaultReporterFactory
         // Merge all the stats for tests from listeners
         for ( TestSetRunListener listener : listeners )
         {
+            // this method should not be here
+            // it should be centralized in one place in the state machine - processor
             for ( TestMethodStats methodStats : listener.getTestMethodStats() )
             {
                 List<TestMethodStats> currentMethodStats =
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportEntryType.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportEntryType.java
index ffdb70653..b785523ef 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportEntryType.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportEntryType.java
@@ -19,13 +19,18 @@ package org.apache.maven.plugin.surefire.report;
  * under the License.
  */
 
+import static java.util.Objects.requireNonNull;
+
 /**
- * Type of an entry in the report
+ * Type of entry in the report.
  *
  */
 public enum ReportEntryType
 {
-
+    TEST_SET_STARTING(),
+    TEST_SET_COMPLETED(),
+    TEST_STARTING(),
+    TEST_ASSUMPTION_FAILURE(),
     ERROR( "error", "flakyError", "rerunError" ),
     FAILURE( "failure", "flakyFailure", "rerunFailure" ),
     SKIPPED( "skipped", "", "" ),
@@ -37,6 +42,11 @@ public enum ReportEntryType
 
     private final String rerunXmlTag;
 
+    ReportEntryType()
+    {
+        this( null, null, null );
+    }
+
     ReportEntryType( String xmlTag, String flakyXmlTag, String rerunXmlTag )
     {
         this.xmlTag = xmlTag;
@@ -46,16 +56,16 @@ public enum ReportEntryType
 
     public String getXmlTag()
     {
-        return xmlTag;
+        return requireNonNull( xmlTag );
     }
 
     public String getFlakyXmlTag()
     {
-        return flakyXmlTag;
+        return requireNonNull( flakyXmlTag );
     }
 
     public String getRerunXmlTag()
     {
-        return rerunXmlTag;
+        return requireNonNull( rerunXmlTag );
     }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportersAggregator.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportersAggregator.java
new file mode 100644
index 000000000..90eabe858
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ReportersAggregator.java
@@ -0,0 +1,135 @@
+package org.apache.maven.plugin.surefire.report;
+
+/*
+ * 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.
+ */
+
+import org.apache.maven.plugin.surefire.runorder.StatisticsReporter;
+import org.apache.maven.surefire.extensions.ConsoleOutputReportEventListener;
+import org.apache.maven.surefire.extensions.StatelessReportEventListener;
+import org.apache.maven.surefire.extensions.StatelessTestSetSummaryListener;
+import org.apache.maven.surefire.extensions.StatelessTestsetInfoConsoleReportEventListener;
+import org.apache.maven.surefire.extensions.StatelessTestsetInfoFileReportEventListener;
+
+/**
+ *
+ */
+public final class ReportersAggregator
+{
+    private StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> consoleReporter;
+    private StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> fileReporter;
+    private StatelessReportEventListener<WrappedReportEntry, TestSetStats> simpleXMLReporter;
+    private ConsoleOutputReportEventListener testOutputReceiver;
+    private StatisticsReporter statisticsReporter;
+    private StatelessTestSetSummaryListener testSetSummaryReporter;
+    private boolean trimStackTrace;
+    private boolean isPlainFormat;
+    private boolean briefOrPlainFormat;
+
+    public StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> getConsoleReporter()
+    {
+        return consoleReporter;
+    }
+
+    public void setConsoleReporter(
+        StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> consoleReporter )
+    {
+        this.consoleReporter = consoleReporter;
+    }
+
+    public StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> getFileReporter()
+    {
+        return fileReporter;
+    }
+
+    public void setFileReporter(
+        StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> fileReporter )
+    {
+        this.fileReporter = fileReporter;
+    }
+
+    public StatelessReportEventListener<WrappedReportEntry, TestSetStats> getSimpleXMLReporter()
+    {
+        return simpleXMLReporter;
+    }
+
+    public void setSimpleXMLReporter( StatelessReportEventListener<WrappedReportEntry, TestSetStats> simpleXMLReporter )
+    {
+        this.simpleXMLReporter = simpleXMLReporter;
+    }
+
+    public ConsoleOutputReportEventListener getTestOutputReceiver()
+    {
+        return testOutputReceiver;
+    }
+
+    public void setTestOutputReceiver( ConsoleOutputReportEventListener testOutputReceiver )
+    {
+        this.testOutputReceiver = testOutputReceiver;
+    }
+
+    public StatisticsReporter getStatisticsReporter()
+    {
+        return statisticsReporter;
+    }
+
+    public void setStatisticsReporter( StatisticsReporter statisticsReporter )
+    {
+        this.statisticsReporter = statisticsReporter;
+    }
+
+    public StatelessTestSetSummaryListener getTestSetSummaryReporter()
+    {
+        return testSetSummaryReporter;
+    }
+
+    public void setTestSetSummaryReporter( StatelessTestSetSummaryListener testSetSummaryReporter )
+    {
+        this.testSetSummaryReporter = testSetSummaryReporter;
+    }
+
+    public boolean isTrimStackTrace()
+    {
+        return trimStackTrace;
+    }
+
+    public void setTrimStackTrace( boolean trimStackTrace )
+    {
+        this.trimStackTrace = trimStackTrace;
+    }
+
+    public boolean isPlainFormat()
+    {
+        return isPlainFormat;
+    }
+
+    public void setPlainFormat( boolean plainFormat )
+    {
+        isPlainFormat = plainFormat;
+    }
+
+    public boolean isBriefOrPlainFormat()
+    {
+        return briefOrPlainFormat;
+    }
+
+    public void setBriefOrPlainFormat( boolean briefOrPlainFormat )
+    {
+        this.briefOrPlainFormat = briefOrPlainFormat;
+    }
+}
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 45f1c5003..007a43be5 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
@@ -20,6 +20,8 @@ package org.apache.maven.plugin.surefire.report;
  */
 
 import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
+import org.apache.maven.surefire.extensions.ReportData;
+import org.apache.maven.surefire.api.report.UniqueID;
 import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter;
 import org.apache.maven.surefire.shared.utils.xml.XMLWriter;
 import org.apache.maven.surefire.extensions.StatelessReportEventListener;
@@ -27,7 +29,6 @@ import org.apache.maven.surefire.api.report.SafeThrowable;
 
 import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -43,6 +44,7 @@ import java.util.StringTokenizer;
 import java.util.concurrent.ConcurrentLinkedDeque;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.newOutputStream;
 import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
 import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
@@ -134,10 +136,22 @@ public class StatelessXmlReporter
         this.phrasedMethodName = phrasedMethodName;
     }
 
+    @Override
+    public void testSetCompleted( UniqueID sourceId, ReportData testSetStats )
+    {
+        TestMethodCalls methodCalls = new TestMethodCalls();
+
+        testSetStats.filterOperations( sourceId )
+            .forEach( methodCalls::addOperation );
+
+        testSetStats.filterRerunOperations( sourceId )
+            .forEach( methodCalls::addRerunOperation );
+    }
+
     @Override
     public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
     {
-        Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
+        Map<UniqueID, Map<UniqueID, List<WrappedReportEntry>>> classMethodStatistics =
                 arrangeMethodStatistics( testSetReportEntry, testSetStats );
 
         // The Java Language Spec:
@@ -151,9 +165,9 @@ public class StatelessXmlReporter
 
             showProperties( ppw, testSetReportEntry.getSystemProperties() );
 
-            for ( Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet() )
+            for ( Entry<UniqueID, Map<UniqueID, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet() )
             {
-                for ( Entry<String, List<WrappedReportEntry>> thisMethodRuns : statistics.getValue().entrySet() )
+                for ( Entry<UniqueID, List<WrappedReportEntry>> thisMethodRuns : statistics.getValue().entrySet() )
                 {
                     serializeTestClass( outputStream, fw, ppw, thisMethodRuns.getValue() );
                 }
@@ -171,38 +185,33 @@ public class StatelessXmlReporter
         }
     }
 
-    private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
-            WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
+    private Map<UniqueID, Map<UniqueID, List<WrappedReportEntry>>> arrangeMethodStatistics(
+        UniqueID sourceId, TestMethodCalls methodCalls )
     {
-        Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
-        for ( WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns( testSetReportEntry, testSetStats ) )
+        Map<UniqueID, TestMethodCalls> methodStatistics = new LinkedHashMap<>();
+
+        for ( WrappedReportEntry methodEntry : methodCalls )
         {
-            String testClassName = methodEntry.getSourceName();
-            Map<String, List<WrappedReportEntry>> stats = classMethodStatistics.get( testClassName );
-            if ( stats == null )
-            {
-                stats = new LinkedHashMap<>();
-                classMethodStatistics.put( testClassName, stats );
-            }
-            String methodName = methodEntry.getName();
-            List<WrappedReportEntry> methodRuns = stats.get( methodName );
-            if ( methodRuns == null )
-            {
-                methodRuns = new ArrayList<>();
-                stats.put( methodName, methodRuns );
-            }
+            UniqueID methodId = methodEntry.getTestRunUniqueId();
+
+            Map<UniqueID, List<WrappedReportEntry>> stats =
+                classMethodStatistics.computeIfAbsent( methodId.toSourceUniqueId(), k -> new LinkedHashMap<>() );
+
+            List<WrappedReportEntry> methodRuns =
+                stats.computeIfAbsent( methodId, k -> new ArrayList<>() );
+
             methodRuns.add( methodEntry );
         }
         return classMethodStatistics;
     }
 
     private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns( WrappedReportEntry testSetReportEntry,
-                                                                       TestSetStats testSetStats )
+                                                                        TestSetStats testSetStats )
     {
-        String suiteClassName = testSetReportEntry.getSourceName();
+        /*String suiteClassName = testSetReportEntry.getSourceName();
         Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap( suiteClassName );
         methodRunHistory.addAll( testSetStats.getReportEntries() );
-        return methodRunHistory;
+        return methodRunHistory;*/
     }
 
     private void serializeTestClass( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
@@ -364,7 +373,7 @@ public class StatelessXmlReporter
         reportFile.delete();
         //noinspection ResultOfMethodCallIgnored
         reportDir.mkdirs();
-        return new BufferedOutputStream( new FileOutputStream( reportFile ), 64 * 1024 );
+        return new BufferedOutputStream( newOutputStream( reportFile.toPath() ), 64 * 1024 );
     }
 
     private static OutputStreamWriter getWriter( OutputStream fos )
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestMethodCalls.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestMethodCalls.java
new file mode 100644
index 000000000..4a1c5dcc5
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestMethodCalls.java
@@ -0,0 +1,40 @@
+package org.apache.maven.plugin.surefire.report;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.maven.surefire.api.report.UniqueID;
+import org.apache.maven.surefire.extensions.ReportData;
+import org.apache.maven.surefire.extensions.testoperations.TestOperation;
+
+import static java.util.stream.Collectors.toMap;
+
+final class TestMethodCalls
+{
+    private final ReportData reportData = new ReportData();
+
+    void addOperation( TestOperation<?> op )
+    {
+        reportData.addOperation( op );
+    }
+
+    void addRerunOperation( TestOperation<?> op )
+    {
+        reportData.addRetryOperation( op );
+    }
+
+    Map<UniqueID, ReportData> mapTestStats()
+    {
+        return reportData.getIds()
+            .stream()
+            .collect( toMap(
+                id -> id,
+                id ->
+                {
+                    ReportData rep = new ReportData();
+                    reportData.filterOperations( id ).forEach( rep::addOperation );
+                    reportData.filterRerunOperations( id ).forEach( rep::addRetryOperation );
+                    return rep;
+                }, (u, v) -> { throw new IllegalStateException(); }, LinkedHashMap::new ) );
+    }
+}
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java
index 2884f68a5..8463d8533 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java
@@ -20,25 +20,36 @@ package org.apache.maven.plugin.surefire.report;
  */
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
 import org.apache.maven.plugin.surefire.runorder.StatisticsReporter;
 import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.RunMode;
 import org.apache.maven.surefire.api.report.TestOutputReportEntry;
 import org.apache.maven.surefire.api.report.TestReportListener;
 import org.apache.maven.surefire.api.report.TestSetReportEntry;
+import org.apache.maven.surefire.extensions.StatelessTestSetSummaryListener;
+import org.apache.maven.surefire.extensions.TestOutputReportOperation;
+import org.apache.maven.surefire.extensions.TestSetReportOperation;
 import org.apache.maven.surefire.extensions.ConsoleOutputReportEventListener;
+import org.apache.maven.surefire.extensions.TestReportOperation;
 import org.apache.maven.surefire.extensions.StatelessReportEventListener;
 import org.apache.maven.surefire.extensions.StatelessTestsetInfoConsoleReportEventListener;
 import org.apache.maven.surefire.extensions.StatelessTestsetInfoFileReportEventListener;
+import org.apache.maven.surefire.extensions.testoperations.TestOperation;
 
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.FAILURE;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
+import static org.apache.maven.surefire.api.report.RunMode.RERUN_TEST_AFTER_FAILURE;
 
 /**
  * Reports data for a single test set.
@@ -49,9 +60,8 @@ import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
 public class TestSetRunListener
     implements TestReportListener<TestOutputReportEntry>
 {
-    private final Queue<TestMethodStats> testMethodStats = new ConcurrentLinkedQueue<>();
-
-    private final TestSetStats detailsForThis;
+    private final Map<Integer, List<TestOperation<?>>> operationsPerSource = new HashMap<>();
+    private final Map<Integer, List<TestOperation<?>>> rerunOperationsPerSource = new HashMap<>();
 
     private final ConsoleOutputReportEventListener testOutputReceiver;
 
@@ -65,29 +75,20 @@ public class TestSetRunListener
 
     private final StatisticsReporter statisticsReporter;
 
-    private final Object lock;
-
-    private Utf8RecodingDeferredFileOutputStream testStdOut = initDeferred( "stdout" );
+    private final StatelessTestSetSummaryListener testSetSummaryReport;
 
-    private Utf8RecodingDeferredFileOutputStream testStdErr = initDeferred( "stderr" );
+    private final Object lock;
 
-    @SuppressWarnings( "checkstyle:parameternumber" )
-    public TestSetRunListener( StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats>
-                                           consoleReporter,
-                               StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats>
-                                       fileReporter,
-                               StatelessReportEventListener<WrappedReportEntry, TestSetStats> simpleXMLReporter,
-                               ConsoleOutputReportEventListener testOutputReceiver,
-                               StatisticsReporter statisticsReporter, boolean trimStackTrace,
-                               boolean isPlainFormat, boolean briefOrPlainFormat, Object lock )
+    public TestSetRunListener( ReportersAggregator reporters, Object lock )
     {
-        this.consoleReporter = consoleReporter;
-        this.fileReporter = fileReporter;
-        this.statisticsReporter = statisticsReporter;
-        this.simpleXMLReporter = simpleXMLReporter;
-        this.testOutputReceiver = testOutputReceiver;
-        this.briefOrPlainFormat = briefOrPlainFormat;
-        detailsForThis = new TestSetStats( trimStackTrace, isPlainFormat );
+        consoleReporter = reporters.getConsoleReporter();
+        fileReporter = reporters.getFileReporter();
+        statisticsReporter = reporters.getStatisticsReporter();
+        simpleXMLReporter = reporters.getSimpleXMLReporter();
+        testOutputReceiver = reporters.getTestOutputReceiver();
+        briefOrPlainFormat = reporters.isBriefOrPlainFormat();
+        testSetSummaryReport = reporters.getTestSetSummaryReporter();
+        detailsForThis = new TestSetStats( reporters.isTrimStackTrace(), reporters.isPlainFormat() );
         this.lock = lock;
     }
 
@@ -176,6 +177,7 @@ public class TestSetRunListener
         {
             synchronized ( lock )
             {
+                addEntry( reportEntry.getSourceId(), reportEntry.getRunMode(), new TestOutputReportOperation( reportEntry ) );
                 Utf8RecodingDeferredFileOutputStream stream = reportEntry.isStdOut() ? testStdOut : testStdErr;
                 stream.write( reportEntry.getLog(), reportEntry.isNewLine() );
                 testOutputReceiver.writeTestOutput( reportEntry );
@@ -183,13 +185,14 @@ public class TestSetRunListener
         }
         catch ( IOException e )
         {
-            throw new RuntimeException( e );
+            throw new UncheckedIOException( e );
         }
     }
 
     @Override
     public void testSetStarting( TestSetReportEntry report )
     {
+        addEntry( report.getSourceId(), report.getRunMode(), new TestSetReportOperation( report ) );
         detailsForThis.testSetStart();
         consoleReporter.testSetStarting( report );
         testOutputReceiver.testSetStarting( report );
@@ -204,6 +207,7 @@ public class TestSetRunListener
     @Override
     public void testSetCompleted( TestSetReportEntry report )
     {
+        addEntry( report.getSourceId(), report.getRunMode(), new TestSetReportOperation( report ) );
         final WrappedReportEntry wrap = wrapTestSet( report );
         final List<String> testResults =
                 briefOrPlainFormat ? detailsForThis.getTestResults() : Collections.<String>emptyList();
@@ -229,12 +233,14 @@ public class TestSetRunListener
     @Override
     public void testStarting( ReportEntry report )
     {
+        addEntry( report.getSourceId(), report.getRunMode(), new TestReportOperation( report ) );
         detailsForThis.testStart();
     }
 
     @Override
     public void testSucceeded( ReportEntry reportEntry )
     {
+        addEntry( reportEntry.getSourceId(), reportEntry.getRunMode(), new TestReportOperation( reportEntry ) );
         WrappedReportEntry wrapped = wrap( reportEntry, SUCCESS );
         detailsForThis.testSucceeded( wrapped );
         statisticsReporter.testSucceeded( reportEntry );
@@ -244,6 +250,7 @@ public class TestSetRunListener
     @Override
     public void testError( ReportEntry reportEntry )
     {
+        addEntry( reportEntry.getSourceId(), reportEntry.getRunMode(), new TestReportOperation( reportEntry ) );
         WrappedReportEntry wrapped = wrap( reportEntry, ERROR );
         detailsForThis.testError( wrapped );
         statisticsReporter.testError( reportEntry );
@@ -253,6 +260,7 @@ public class TestSetRunListener
     @Override
     public void testFailed( ReportEntry reportEntry )
     {
+        addEntry( reportEntry.getSourceId(), reportEntry.getRunMode(), new TestReportOperation( reportEntry ) );
         WrappedReportEntry wrapped = wrap( reportEntry, FAILURE );
         detailsForThis.testFailure( wrapped );
         statisticsReporter.testFailed( reportEntry );
@@ -266,6 +274,7 @@ public class TestSetRunListener
     @Override
     public void testSkipped( ReportEntry reportEntry )
     {
+        addEntry( reportEntry.getSourceId(), reportEntry.getRunMode(), new TestReportOperation( reportEntry ) );
         WrappedReportEntry wrapped = wrap( reportEntry, SKIPPED );
         detailsForThis.testSkipped( wrapped );
         statisticsReporter.testSkipped( reportEntry );
@@ -280,6 +289,7 @@ public class TestSetRunListener
     @Override
     public void testAssumptionFailure( ReportEntry report )
     {
+        addEntry( report.getSourceId(), report.getRunMode(), new TestReportOperation( report ) );
         testSkipped( report );
     }
 
@@ -323,6 +333,18 @@ public class TestSetRunListener
         return testMethodStats;
     }
 
+    private void addEntry( Integer source, RunMode runMode, TestOperation<?> operation )
+    {
+        Map<Integer, List<TestOperation<?>>> sourceOperations =
+            runMode == RERUN_TEST_AFTER_FAILURE ? rerunOperationsPerSource : operationsPerSource;
+        sourceOperations.compute( source, ( k, v ) ->
+        {
+            List<TestOperation<?>> operations = v == null ? new ArrayList<>() : v;
+            operations.add( operation );
+            return operations;
+        } );
+    }
+
     private static String trimTrailingNewLine( final String message )
     {
         final int e = message == null ? 0 : lineBoundSymbolWidth( message );
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestStatsProcessor.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestStatsProcessor.java
new file mode 100644
index 000000000..1c7ec99da
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestStatsProcessor.java
@@ -0,0 +1,5 @@
+package org.apache.maven.plugin.surefire.report;
+
+public class TestStatsProcessor
+{
+}
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java
index bc2fca0c5..5e360b8eb 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java
@@ -23,6 +23,7 @@ import org.apache.maven.surefire.api.report.ReportEntry;
 import org.apache.maven.surefire.api.report.RunMode;
 import org.apache.maven.surefire.api.report.StackTraceWriter;
 import org.apache.maven.surefire.api.report.TestSetReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
 
 import javax.annotation.Nonnull;
 import java.util.Collections;
@@ -239,6 +240,11 @@ public class WrappedReportEntry
         return original.getTestRunId();
     }
 
+    public UniqueID getTestRunUniqueId()
+    {
+        return getTestRunId() == null ? null : new UniqueID( getTestRunId() );
+    }
+
     @Override
     public Map<String, String> getSystemProperties()
     {
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/ReportEntry.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/ReportEntry.java
index ed3fdc5a1..3c2a9b983 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/ReportEntry.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/ReportEntry.java
@@ -21,6 +21,9 @@ package org.apache.maven.surefire.api.report;
 
 import javax.annotation.Nonnull;
 
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toNameId;
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toSourceId;
+
 /**
  * Describes a single entry for a test report
  *
@@ -41,6 +44,12 @@ public interface ReportEntry
      */
     String getSourceText();
 
+    default Integer getSourceId()
+    {
+        Long id = getTestRunId();
+        return id == null ? null : toSourceId( id );
+    }
+
     /**
      * The name of the test case
      *
@@ -55,6 +64,12 @@ public interface ReportEntry
      */
     String getNameText();
 
+    default Integer getNameId()
+    {
+        Long id = getTestRunId();
+        return id == null ? null : toNameId( id );
+    }
+
     /**
      * The group/category of the testcase
      *
@@ -123,4 +138,10 @@ public interface ReportEntry
      * @return id
      */
     Long getTestRunId();
+
+    default UniqueID getUniqueId()
+    {
+        Long id = getTestRunId();
+        return id == null ? null : new UniqueID( id );
+    }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/TestOutputReportEntry.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/TestOutputReportEntry.java
index 3610f85c2..411956f6b 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/TestOutputReportEntry.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/TestOutputReportEntry.java
@@ -19,6 +19,9 @@ package org.apache.maven.surefire.api.report;
  * under the License.
  */
 
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toNameId;
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toSourceId;
+
 /**
  * This report entry should be used in {@link TestOutputReceiver#writeTestOutput(OutputReportEntry)}.
  *
@@ -118,4 +121,14 @@ public final class TestOutputReportEntry implements OutputReportEntry
     {
         return new TestOutputReportEntry( log, false, true );
     }
+
+    public Integer getSourceId()
+    {
+        return toSourceId( getTestRunId() );
+    }
+
+    public Integer getNameId()
+    {
+        return toNameId( getTestRunId() );
+    }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/UniqueID.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/UniqueID.java
new file mode 100644
index 000000000..d9d58e1b6
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/UniqueID.java
@@ -0,0 +1,70 @@
+package org.apache.maven.surefire.api.report;
+
+/*
+ * 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.
+ */
+
+import java.util.Objects;
+
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toSourceId;
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toTestRunId;
+
+/**
+ *
+ */
+public final class UniqueID
+{
+    private final Long id;
+
+    public UniqueID( int sourceId, Integer testId )
+    {
+        this( testId == null ? toTestRunId( sourceId ) : toTestRunId( sourceId, testId ) );
+    }
+
+    public UniqueID( Long id )
+    {
+        this.id = id;
+
+    }
+
+    public UniqueID toSourceUniqueId()
+    {
+        return new UniqueID( id == null ? null : toTestRunId( toSourceId( id ) ) );
+    }
+
+    @Override
+    public boolean equals( Object o )
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+        UniqueID uniqueID = (UniqueID) o;
+        return Objects.equals( id, uniqueID.id );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash( id );
+    }
+}
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ReportEntryUtils.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ReportEntryUtils.java
new file mode 100644
index 000000000..423c4af38
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ReportEntryUtils.java
@@ -0,0 +1,79 @@
+package org.apache.maven.surefire.api.util.internal;
+
+/*
+ * 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.
+ */
+
+/**
+ * Utility class for {@link org.apache.maven.surefire.api.report.ReportEntry}.
+ */
+public final class ReportEntryUtils
+{
+    private ReportEntryUtils()
+    {
+        throw new IllegalStateException( "no instantiable constructor" );
+    }
+
+    /**
+     * @param sourceId class id or source id (parent)
+     * @param testId   test child
+     * @return shifts {@code sourceId} to 32-bit MSB of long and encodes {@code testId} to LSB
+     */
+    public static long toTestRunId( int sourceId, int testId )
+    {
+        return toTestRunId( sourceId ) | testId;
+    }
+
+    /**
+     * @param sourceId class id or source id (parent)
+     * @return shifts in 32 bits
+     */
+    public static long toTestRunId( int sourceId )
+    {
+        return ( (long) sourceId ) << 32;
+    }
+
+    /**
+     * @param testRunId encoded 32-bit MSB source and 32-bit LSB name in 64-bit value
+     * @return shifts {@code testRunId} in 32 bits right
+     */
+    public static int toSourceId( long testRunId )
+    {
+        return (int) ( 0x00000000ffffffffL & ( testRunId >>> 32 ) );
+    }
+
+    public static boolean existsSourceId( Long testRunId )
+    {
+        return testRunId != null && toSourceId( testRunId ) != 0;
+    }
+
+    /**
+     *
+     * @param testRunId encoded 32-bit MSB source and 32-bit LSB name in 64-bit value
+     * @return 32-bit LSB of {@code testRunId}
+     */
+    public static int toNameId( long testRunId )
+    {
+        return (int) ( 0x00000000ffffffffL & testRunId );
+    }
+
+    public static boolean existsNameId( Long testRunId )
+    {
+        return testRunId != null && toNameId( testRunId ) != 0;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ReportData.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ReportData.java
new file mode 100644
index 000000000..52f38edc3
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ReportData.java
@@ -0,0 +1,96 @@
+package org.apache.maven.surefire.extensions;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.maven.surefire.api.report.UniqueID;
+import org.apache.maven.surefire.extensions.testoperations.TestOperation;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toCollection;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ *
+ */
+public final class ReportData
+{
+    private final List<TestOperation<?>> operations = new ArrayList<>();
+    private final List<TestOperation<?>> rerunOperations = new ArrayList<>();
+
+    public void addOperation( TestOperation<?> op )
+    {
+        operations.add( requireNonNull( op ) );
+    }
+
+    public void addRetryOperation( TestOperation<?> op )
+    {
+        rerunOperations.add( requireNonNull( op ) );
+    }
+
+    public Set<UniqueID> getIds()
+    {
+        return operations.stream()
+            .map( TestOperation::getSourceId )
+            .collect( toCollection( LinkedHashSet::new ) );
+    }
+
+    public List<TestOperation<?>> filterOperations( UniqueID sourceId )
+    {
+        return filterBySourceId( sourceId, operations );
+    }
+
+    public List<TestOperation<?>> filterRerunOperations( UniqueID sourceId )
+    {
+        return filterBySourceId( sourceId, rerunOperations );
+    }
+
+    public void removeSourceId( UniqueID sourceId )
+    {
+        removeBySourceId( sourceId, operations.iterator() );
+        removeBySourceId( sourceId, rerunOperations.iterator() );
+    }
+
+    private List<TestOperation<?>> filterBySourceId( UniqueID sourceId, List<TestOperation<?>> sources )
+    {
+        return sources.stream()
+            .filter( op -> op.getSourceId().equals( sourceId ) )
+            .collect( toList() );
+    }
+
+    private void removeBySourceId( UniqueID sourceId, Iterator<TestOperation<?>> it )
+    {
+        for ( TestOperation<?> op; it.hasNext(); )
+        {
+            op = it.next();
+            if ( op.getSourceId().equals( sourceId ) )
+            {
+                it.remove();
+            }
+        }
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessReportEventListener.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessReportEventListener.java
index 20eda3bdb..4eadc8c71 100644
--- a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessReportEventListener.java
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessReportEventListener.java
@@ -19,7 +19,7 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import org.apache.maven.surefire.api.report.TestSetReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
 
 /**
  * Creates a report upon handled event "<em>testSetCompleted</em>".
@@ -28,16 +28,12 @@ import org.apache.maven.surefire.api.report.TestSetReportEntry;
  *
  * author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M4
- * @param <R> report entry type, see <em>WrappedReportEntry</em> from module the <em>maven-surefire-common</em>
- * @param <S> test-set statistics, see <em>TestSetStats</em> from module the <em>maven-surefire-common</em>
  */
-public interface StatelessReportEventListener<R extends TestSetReportEntry, S>
+public interface StatelessReportEventListener
 {
     /**
      * The callback is called after the test class has been completed and the state of report is final.
-     *
-     * @param report <em>WrappedReportEntry</em>
-     * @param testSetStats <em>TestSetStats</em>
+     * @param testSetStats <em>StatelessReportData</em>
      */
-     void testSetCompleted( R report, S testSetStats );
+     void testSetCompleted( UniqueID sourceId, ReportData testSetStats );
 }
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessTestSetSummaryListener.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessTestSetSummaryListener.java
new file mode 100644
index 000000000..9608440c7
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/StatelessTestSetSummaryListener.java
@@ -0,0 +1,6 @@
+package org.apache.maven.surefire.extensions;
+
+public interface StatelessTestSetSummaryListener
+{
+    void testSetCompleted(  );
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestAssumptionFailureOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestAssumptionFailureOperation.java
new file mode 100644
index 000000000..61f9a5f91
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestAssumptionFailureOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestAssumptionFailureOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestAssumptionFailureOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestErrorOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestErrorOperation.java
new file mode 100644
index 000000000..b54e5c741
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestErrorOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestErrorOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestErrorOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestExecutionSkippedByUserOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestExecutionSkippedByUserOperation.java
new file mode 100644
index 000000000..9b51178d7
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestExecutionSkippedByUserOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestExecutionSkippedByUserOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestExecutionSkippedByUserOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestFailedOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestFailedOperation.java
new file mode 100644
index 000000000..971522a34
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestFailedOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestFailedOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestFailedOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestOperation.java
new file mode 100644
index 000000000..d6e43c7d4
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestOperation.java
@@ -0,0 +1,15 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public abstract class TestOperation<T extends ReportEntry>
+{
+    private final long createdAt = System.currentTimeMillis();
+    public abstract UniqueID getSourceId();
+
+    public final long createdAt()
+    {
+        return createdAt;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSetCompletedOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSetCompletedOperation.java
new file mode 100644
index 000000000..333bf21c6
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSetCompletedOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.TestSetReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestSetCompletedOperation extends TestOperation<TestSetReportEntry>
+{
+    private final TestSetReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestSetCompletedOperation( TestSetReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSetStartingOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSetStartingOperation.java
new file mode 100644
index 000000000..00fa1b5d9
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSetStartingOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.TestSetReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestSetStartingOperation extends TestOperation<TestSetReportEntry>
+{
+    private final TestSetReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestSetStartingOperation( TestSetReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSkippedOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSkippedOperation.java
new file mode 100644
index 000000000..36c3d70db
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSkippedOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestSkippedOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestSkippedOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestStartingOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestStartingOperation.java
new file mode 100644
index 000000000..a3250a254
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestStartingOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestStartingOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestStartingOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSucceededOperation.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSucceededOperation.java
new file mode 100644
index 000000000..65f28bc75
--- /dev/null
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/testoperations/TestSucceededOperation.java
@@ -0,0 +1,22 @@
+package org.apache.maven.surefire.extensions.testoperations;
+
+import org.apache.maven.surefire.api.report.ReportEntry;
+import org.apache.maven.surefire.api.report.UniqueID;
+
+public final class TestSucceededOperation extends TestOperation<ReportEntry>
+{
+    private final ReportEntry event;
+    private final UniqueID sourceId;
+
+    public TestSucceededOperation( ReportEntry event )
+    {
+        this.event = event;
+        sourceId = event.getUniqueId();
+    }
+
+    @Override
+    public UniqueID getSourceId()
+    {
+        return sourceId;
+    }
+}
diff --git a/surefire-providers/common-java5/src/main/java/org/apache/maven/surefire/report/ClassMethodIndexer.java b/surefire-providers/common-java5/src/main/java/org/apache/maven/surefire/report/ClassMethodIndexer.java
index 8e58d7a4c..d9818e460 100644
--- a/surefire-providers/common-java5/src/main/java/org/apache/maven/surefire/report/ClassMethodIndexer.java
+++ b/surefire-providers/common-java5/src/main/java/org/apache/maven/surefire/report/ClassMethodIndexer.java
@@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static java.util.Objects.requireNonNull;
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toSourceId;
+import static org.apache.maven.surefire.api.util.internal.ReportEntryUtils.toTestRunId;
 
 /**
  * Creates an index for class/method.
@@ -44,10 +46,10 @@ public final class ClassMethodIndexer
         return testIdMapping.computeIfAbsent( key, cm ->
         {
             Long classId = testIdMapping.get( new ClassMethod( requireNonNull( clazz ), null ) );
-            long c = classId == null ? ( ( (long) classIndex.getAndIncrement() ) << 32 ) : classId;
+            int c = classId == null ? classIndex.getAndIncrement() : toSourceId( classId );
             int m = method == null ? 0 : methodIndex.getAndIncrement();
             long id = c | m;
-            testLocalMapping.set( id );
+            testLocalMapping.set( toTestRunId( c, m ) );
             return id;
         } );
     }
@@ -61,4 +63,9 @@ public final class ClassMethodIndexer
     {
         return testLocalMapping.get();
     }
+
+    public void removeLocalIndex()
+    {
+        testLocalMapping.remove();
+    }
 }
diff --git a/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProvider.java b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProvider.java
index ad2ec944d..23ddb83ce 100644
--- a/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProvider.java
+++ b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProvider.java
@@ -180,10 +180,10 @@ public class JUnitPlatformProvider
         }
         // Rerun failing tests if requested
         int count = parameters.getTestRequest().getRerunFailingTestsCount();
-        if ( count > 0 && adapter.hasFailingTests() )
+        if ( count > 0 )
         {
             adapter.setRunMode( RERUN_TEST_AFTER_FAILURE );
-            for ( int i = 0; i < count; i++ )
+            for ( int i = 0; i < count && adapter.hasFailingTests(); i++ )
             {
                 try
                 {
@@ -193,11 +193,6 @@ public class JUnitPlatformProvider
                     // Reset adapter's recorded failures and invoke the failed tests again
                     adapter.reset();
                     launcher.execute( discoveryRequest, adapter );
-                    // If no tests fail in the rerun, we're done
-                    if ( !adapter.hasFailingTests() )
-                    {
-                        break;
-                    }
                 }
                 finally
                 {
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 aeb24576e..14cf13bb7 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
@@ -172,6 +172,7 @@ final class RunListenerAdapter
                                 createReportEntry( testIdentifier, null, systemProps(), null, elapsed ) );
                     }
             }
+            classMethodIndexer.removeLocalIndex();
         }
 
         runningTestIdentifiersByUniqueId.remove( testIdentifier.getUniqueId() );
@@ -234,9 +235,14 @@ final class RunListenerAdapter
         {
             methodText = null;
         }
+
         StackTraceWriter stw =
                 testExecutionResult == null ? null : toStackTraceWriter( className, methodName, testExecutionResult );
-        return new SimpleReportEntry( runMode, classMethodIndexer.indexClassMethod( className, methodName ), className,
+
+        long uniqueId = classMethodIndexer.indexClassMethod(
+            testIdentifier.getParentId().orElse( className ), testIdentifier.getUniqueId() );
+
+        return new SimpleReportEntry( runMode, uniqueId, className,
             classText, methodName, methodText, stw, elapsedTime, reason, systemProperties );
     }