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 2018/05/03 20:02:31 UTC

[maven-surefire] 01/01: [SUREFIRE-1330] JUnit 5 surefire-provider code donation

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

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

commit e61f2bef5a7f840cf7a57dde63c05f166235e37e
Author: Tibor17 <ti...@apache.org>
AuthorDate: Fri Apr 13 23:51:33 2018 +0200

    [SUREFIRE-1330] JUnit 5 surefire-provider code donation
---
 .../plugin/surefire/AbstractSurefireMojo.java      |  46 +-
 pom.xml                                            |   7 +-
 surefire-its/pom.xml                               |  23 +-
 .../maven/surefire/its/JUnit4VersionsIT.java       |  74 +-
 .../org/apache/maven/surefire/its/JUnit5IT.java}   |  50 +-
 .../maven/surefire/its/JUnitPlatformIT.java}       |  48 +-
 .../apache/maven/surefire/its/JUnitVersion.java    |  80 +++
 .../resources/{junit4 => junit-platform}/pom.xml   |  55 +-
 .../src/test/java/junitplatform}/BasicTest.java    |  30 +-
 surefire-its/src/test/resources/junit4/pom.xml     | 141 +++-
 .../junit4/src/test/java/junit4/BasicTest.java     |   4 +-
 .../BasicTest.java => junit5/JUnit5Test.java}      |  35 +-
 surefire-its/src/test/resources/junit5/pom.xml     |  77 +++
 .../src/test/java/junit5/JUnit4Test.java}          |  19 +-
 .../src/test/java/junit5/JUnit5Test.java}          |  36 +-
 surefire-providers/pom.xml                         |   1 +
 surefire-providers/surefire-junit-platform/pom.xml | 161 +++++
 .../junitplatform/JUnitPlatformProvider.java       | 241 +++++++
 .../surefire/junitplatform/RunListenerAdapter.java | 272 ++++++++
 .../surefire/junitplatform/TestMethodFilter.java   |  60 ++
 .../junitplatform/TestPlanScannerFilter.java       |  60 ++
 ...che.maven.surefire.providerapi.SurefireProvider |   1 +
 .../junitplatform/JUnitPlatformProviderTests.java  | 756 +++++++++++++++++++++
 .../junitplatform/RunListenerAdapterTests.java     | 580 ++++++++++++++++
 .../junitplatform/TestMethodFilterTests.java       | 105 +++
 .../junitplatform/TestPlanScannerFilterTests.java  | 188 +++++
 26 files changed, 2910 insertions(+), 240 deletions(-)

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
index 34f805a..414e084 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
@@ -1024,6 +1024,7 @@ public abstract class AbstractSurefireMojo
                               new TestNgProviderInfo( getTestNgArtifact() ),
                               new JUnitCoreProviderInfo( getJunitArtifact(), junitDepArtifact ),
                               new JUnit4ProviderInfo( getJunitArtifact(), junitDepArtifact ),
+                              new JUnitPlatformProviderInfo( getJunitArtifact() ),
                               new JUnit3ProviderInfo() )
             .resolve();
     }
@@ -1540,7 +1541,17 @@ public abstract class AbstractSurefireMojo
         return dependencyResolver.isWithinVersionSpec( artifact, "[4.0,)" );
     }
 
-    static boolean isForkModeNever( String forkMode )
+    private boolean isJunitJupiter( Artifact artifact )
+    {
+        return dependencyResolver.isWithinVersionSpec( artifact, "[5.0.0,)" );
+    }
+
+    private boolean isJunitVintage( Artifact artifact )
+    {
+        return dependencyResolver.isWithinVersionSpec( artifact, "[4.12.0,5.0.0)" );
+    }
+
+    private static boolean isForkModeNever( String forkMode )
     {
         return FORK_NEVER.equals( forkMode );
     }
@@ -2873,6 +2884,39 @@ public abstract class AbstractSurefireMojo
 
     }
 
+    final class JUnitPlatformProviderInfo
+        implements ProviderInfo
+    {
+        private final Artifact junitArtifact;
+
+        JUnitPlatformProviderInfo( Artifact junitArtifact )
+        {
+            this.junitArtifact = junitArtifact;
+        }
+
+        @Nonnull public String getProviderName()
+        {
+            return "org.apache.maven.surefire.junitplatform.JUnitPlatformProvider";
+        }
+
+        public boolean isApplicable()
+        {
+            return isJunitJupiter( junitArtifact ) || isJunitVintage( junitArtifact );
+        }
+
+        public void addProviderProperties() throws MojoExecutionException
+        {
+        }
+
+        public Classpath getProviderClasspath()
+            throws ArtifactResolutionException, ArtifactNotFoundException
+        {
+            return dependencyResolver.getProviderClasspath( "surefire-junit-platform",
+                                                            surefireBooterArtifact.getBaseVersion(),
+                                                            null );
+        }
+    }
+
     final class JUnitCoreProviderInfo
         implements ProviderInfo
     {
diff --git a/pom.xml b/pom.xml
index 5fdefd4..96a75a4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,8 +97,9 @@
     <maven.site.path>surefire-archives/surefire-LATEST</maven.site.path>
     <!-- Override with Jigsaw JRE 9 -->
     <jdk.home>${java.home}/..</jdk.home>
-    <maven.compiler.testSource>1.6</maven.compiler.testSource>
-    <maven.compiler.testTarget>1.6</maven.compiler.testTarget>
+    <javaVersion>6</javaVersion>
+    <maven.compiler.testSource>1.${javaVersion}</maven.compiler.testSource>
+    <maven.compiler.testTarget>1.${javaVersion}</maven.compiler.testTarget>
     <jvm.args.tests>-server -XX:+UseG1GC -Xms128m -Xmx144m -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:SoftRefLRUPolicyMSPerMB=50 -Djava.awt.headless=true</jvm.args.tests>
   </properties>
 
@@ -419,6 +420,7 @@
                 <excludeDependencies>
                   <param>org.codehaus.plexus:plexus-java</param>
                   <param>org.ow2.asm:asm</param>
+                  <param>org.junit.platform:junit-platform-commons</param>
                 </excludeDependencies>
                 <ignores>
                   <param>org.objectweb.asm.*</param>
@@ -550,6 +552,7 @@
                   <excludes>
                     <exclude>org.codehaus.plexus:plexus-java</exclude>
                     <exclude>org.ow2.asm:asm</exclude>
+                    <exclude>org.junit.platform:junit-platform-commons</exclude>
                   </excludes>
                 </enforceBytecodeVersion>
               </rules>
diff --git a/surefire-its/pom.xml b/surefire-its/pom.xml
index 88a4870..d4c7df4 100644
--- a/surefire-its/pom.xml
+++ b/surefire-its/pom.xml
@@ -104,7 +104,9 @@
           <forkMode>once</forkMode>
           <argLine>-server -Xmx64m -XX:+UseG1GC -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Djava.awt.headless=true</argLine>
           <includes>
-            <include>org/apache/**/*IT*.java</include>
+            <include>org/apache/**/JUnit4VersionsIT.java</include>
+            <include>org/apache/**/JUnit5IT.java</include>
+            <include>org/apache/**/JUnitPlatformIT.java</include>
           </includes>
           <!-- Pass current surefire version to the main suite so that it -->
           <!-- can forward to all integration test projects. SUREFIRE-513 -->
@@ -143,25 +145,6 @@
         </executions>
       </plugin>
       <plugin>
-        <artifactId>maven-enforcer-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>require-maven-2.1.0</id>
-            <goals>
-              <goal>enforce</goal>
-            </goals>
-            <configuration>
-              <rules>
-                <requireMavenVersion>
-                  <!-- Some plugin features require a recent Maven runtime to work (e.g. SystemPropertiesTest) -->
-                  <version>[2.1.0,)</version>
-                </requireMavenVersion>
-              </rules>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
         <artifactId>maven-jar-plugin</artifactId>
         <!-- todo dont skip since of failsafe:2.19 internal use if having src/main/java/... -->
         <configuration>
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4VersionsIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4VersionsIT.java
index 8dd8f0c..c391964 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4VersionsIT.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit4VersionsIT.java
@@ -19,7 +19,6 @@ package org.apache.maven.surefire.its;
  * under the License.
  */
 
-import java.util.Arrays;
 import java.util.Collection;
 
 import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
@@ -29,8 +28,28 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 
+import static java.util.Arrays.asList;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_10;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_11;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_12;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_8;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_8_1;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_8_2;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_9;
 import static org.junit.runners.Parameterized.*;
 
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_0;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_1;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_2;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_3;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_3_1;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_4;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_5;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_6;
+import static org.apache.maven.surefire.its.JUnitVersion.JUNIT_4_7;
+import static org.apache.maven.surefire.its.JUnitVersion.JUPITER_5_1_1;
+import static org.apache.maven.surefire.its.JUnitVersion.VINTAGE_5_1_1;
+
 /**
  * Basic suite test using all known versions of JUnit 4.x
  *
@@ -44,44 +63,41 @@ public class JUnit4VersionsIT
     @Parameters( name = "{index}: JUnit {0}" )
     public static Collection<Object[]> junitVersions()
     {
-        return Arrays.asList( new Object[][] {
-                { "4.0" },
-                { "4.1" },
-                { "4.2" },
-                { "4.3" },
-                { "4.3.1" },
-                { "4.4" },
-                { "4.5" },
-                { "4.6" },
-                { "4.7" },
-                { "4.8" },
-                { "4.8.1" },
-                { "4.8.2" },
-                { "4.9" },
-                { "4.10" },
-                { "4.11" },
-                { "4.12" }
+        return asList( new Object[][] {
+                { JUNIT_4_0 },
+                { JUNIT_4_1 },
+                { JUNIT_4_2 },
+                { JUNIT_4_3 },
+                { JUNIT_4_3_1 },
+                { JUNIT_4_4 },
+                { JUNIT_4_5 },
+                { JUNIT_4_6 },
+                { JUNIT_4_7 },
+                { JUNIT_4_8 },
+                { JUNIT_4_8_1 },
+                { JUNIT_4_8_2 },
+                { JUNIT_4_9 },
+                { JUNIT_4_10 },
+                { JUNIT_4_11 },
+                { JUNIT_4_12 },
+                { VINTAGE_5_1_1 },
+                { JUPITER_5_1_1 }
         } );
     }
 
     @Parameter
-    public String version;
-
-    private SurefireLauncher unpack()
-    {
-        return unpack( "/junit4", version );
-    }
+    public JUnitVersion version;
 
     @Test
     public void testJunit()
-        throws Exception
     {
-        runJUnitTest( version );
+        version.configure( unpack() )
+                .executeTest()
+                .verifyErrorFree( 1 );
     }
 
-    public void runJUnitTest( String version )
-        throws Exception
+    private SurefireLauncher unpack()
     {
-        unpack().setJUnitVersion( version ).executeTest().verifyErrorFree( 1 );
+        return unpack( "/junit4", version.toString() );
     }
 }
diff --git a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit5IT.java
similarity index 54%
copy from surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
copy to surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit5IT.java
index e9234f2..8c5eb3a 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnit5IT.java
@@ -1,4 +1,4 @@
-package junit4;
+package org.apache.maven.surefire.its;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,47 +19,27 @@ package junit4;
  * under the License.
  */
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
+import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
 import org.junit.Test;
 
+import static org.apache.maven.surefire.its.fixture.HelperAssertions.assumeJavaVersion;
 
-public class BasicTest
+/**
+ * Basic suite test currently only running against JUnit 5 M2
+ *
+ * @author <a href="mailto:britter@apache.org">Benedikt Ritter</a>
+ */
+public class JUnit5IT
+    extends SurefireJUnit4IntegrationTestCase
 {
 
-    private boolean setUpCalled = false;
-
-    private static boolean tearDownCalled = false;
-    
-    @Before
-    public void setUp()
-    {
-        setUpCalled = true;
-        tearDownCalled = false;
-        System.out.println( "Called setUp" );
-    }
-
-    @After
-    public void tearDown()
-    {
-        setUpCalled = false;
-        tearDownCalled = true;
-        System.out.println( "Called tearDown" );
-    }
-
     @Test
-    public void testSetUp()
+    public void test()
     {
-        Assert.assertTrue( "setUp was not called", setUpCalled );
-    }
-  
+        assumeJavaVersion( 1.8d );
 
-    @AfterClass
-    public static void oneTimeTearDown()
-    {
-        
+        unpack( "/junit5" )
+                .executeTest()
+                .verifyErrorFree( 2 );
     }
-
 }
diff --git a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnitPlatformIT.java
similarity index 53%
copy from surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
copy to surefire-its/src/test/java/org/apache/maven/surefire/its/JUnitPlatformIT.java
index e9234f2..f82206c 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnitPlatformIT.java
@@ -1,4 +1,4 @@
-package junit4;
+package org.apache.maven.surefire.its;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,47 +19,29 @@ package junit4;
  * under the License.
  */
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
+import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
+import org.apache.maven.surefire.its.fixture.SurefireLauncher;
 import org.junit.Test;
 
+import static java.lang.System.getProperty;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assume.assumeThat;
 
-public class BasicTest
+public class JUnitPlatformIT
+    extends SurefireJUnit4IntegrationTestCase
 {
-
-    private boolean setUpCalled = false;
-
-    private static boolean tearDownCalled = false;
-    
-    @Before
-    public void setUp()
-    {
-        setUpCalled = true;
-        tearDownCalled = false;
-        System.out.println( "Called setUp" );
-    }
-
-    @After
-    public void tearDown()
+    private SurefireLauncher unpack()
     {
-        setUpCalled = false;
-        tearDownCalled = true;
-        System.out.println( "Called tearDown" );
+        return unpack( "/junit-platform" );
     }
 
     @Test
-    public void testSetUp()
+    public void test40()
     {
-        Assert.assertTrue( "setUp was not called", setUpCalled );
-    }
-  
+        assumeThat( "java.specification.version: ",
+                getProperty( "java.specification.version" ), is( greaterThanOrEqualTo( "1.8" ) ) );
 
-    @AfterClass
-    public static void oneTimeTearDown()
-    {
-        
+        unpack().executeTest().verifyErrorFree( 1 );
     }
-
 }
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnitVersion.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnitVersion.java
new file mode 100644
index 0000000..8e7f606
--- /dev/null
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/JUnitVersion.java
@@ -0,0 +1,80 @@
+package org.apache.maven.surefire.its;
+
+/*
+ * 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.surefire.its.fixture.SurefireLauncher;
+
+/**
+ * Enum listing all the JUnit version.
+ */
+public enum JUnitVersion {
+
+    JUNIT_4_0( "4.0" ),
+    JUNIT_4_1( "4.1" ),
+    JUNIT_4_2( "4.2" ),
+    JUNIT_4_3( "4.3" ),
+    JUNIT_4_3_1( "4.3.1" ),
+    JUNIT_4_4( "4.4" ),
+    JUNIT_4_5( "4.5" ),
+    JUNIT_4_6( "4.6" ),
+    JUNIT_4_7( "4.7" ),
+    JUNIT_4_8( "4.8" ),
+    JUNIT_4_8_1( "4.8.1" ),
+    JUNIT_4_8_2( "4.8.2" ),
+    JUNIT_4_9( "4.9" ),
+    JUNIT_4_10( "4.10" ),
+    JUNIT_4_11( "4.11" ),
+    JUNIT_4_12( "4.12" ),
+    VINTAGE_5_1_1( "5.1.1" )
+    {
+        @Override
+        public SurefireLauncher configure( SurefireLauncher launcher )
+        {
+            return super.configure( launcher )
+                    .activateProfile( "junit5-vintage" );
+        }
+    },
+    JUPITER_5_1_1( "5.1.1" )
+    {
+        @Override
+        public SurefireLauncher configure( SurefireLauncher launcher )
+        {
+            return super.configure( launcher )
+                    .activateProfile( "junit5-jupiter" );
+        }
+    };
+
+    private final String version;
+
+    JUnitVersion( String version )
+    {
+        this.version = version;
+    }
+
+    public SurefireLauncher configure( SurefireLauncher launcher )
+    {
+        return launcher.setJUnitVersion( version );
+    }
+
+    public String toString()
+    {
+        return version;
+    }
+}
diff --git a/surefire-its/src/test/resources/junit4/pom.xml b/surefire-its/src/test/resources/junit-platform/pom.xml
similarity index 55%
copy from surefire-its/src/test/resources/junit4/pom.xml
copy to surefire-its/src/test/resources/junit-platform/pom.xml
index 751a0e6..40a4f61 100644
--- a/surefire-its/src/test/resources/junit4/pom.xml
+++ b/surefire-its/src/test/resources/junit-platform/pom.xml
@@ -17,39 +17,38 @@
   ~ 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>
+    <modelVersion>4.0.0</modelVersion>
 
-  <groupId>org.apache.maven.plugins.surefire</groupId>
-  <artifactId>junit4</artifactId>
-  <version>1.0-SNAPSHOT</version>
-  <name>Test for JUnit 4</name>
+    <groupId>org.apache.maven.plugins.surefire</groupId>
+    <artifactId>junit-platform</artifactId>
+    <version>1.0</version>
+    <name>Test for JUnit 5 Platform</name>
 
-  <properties>
-    <junitVersion>4.4</junitVersion>
-    <maven.compiler.source>1.6</maven.compiler.source>
-    <maven.compiler.target>1.6</maven.compiler.target>
-  </properties>
+    <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>
+    <dependencies>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.1.1</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/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/resources/junit-platform/src/test/java/junitplatform/BasicTest.java
similarity index 73%
copy from surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
copy to surefire-its/src/test/resources/junit-platform/src/test/java/junitplatform/BasicTest.java
index e9234f2..2c9d119 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/resources/junit-platform/src/test/java/junitplatform/BasicTest.java
@@ -1,4 +1,4 @@
-package junit4;
+package junitplatform;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,21 +19,21 @@ package junit4;
  * under the License.
  */
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 public class BasicTest
 {
 
-    private boolean setUpCalled = false;
+    private boolean setUpCalled;
 
-    private static boolean tearDownCalled = false;
-    
-    @Before
+    private static boolean tearDownCalled;
+
+    @BeforeEach
     public void setUp()
     {
         setUpCalled = true;
@@ -41,7 +41,7 @@ public class BasicTest
         System.out.println( "Called setUp" );
     }
 
-    @After
+    @AfterEach
     public void tearDown()
     {
         setUpCalled = false;
@@ -52,14 +52,14 @@ public class BasicTest
     @Test
     public void testSetUp()
     {
-        Assert.assertTrue( "setUp was not called", setUpCalled );
+        assertTrue( setUpCalled, "setUp was not called" );
     }
-  
 
-    @AfterClass
+
+    @AfterAll
     public static void oneTimeTearDown()
     {
-        
+        assertTrue( tearDownCalled );
     }
 
 }
diff --git a/surefire-its/src/test/resources/junit4/pom.xml b/surefire-its/src/test/resources/junit4/pom.xml
index 751a0e6..deddd56 100644
--- a/surefire-its/src/test/resources/junit4/pom.xml
+++ b/surefire-its/src/test/resources/junit4/pom.xml
@@ -20,36 +20,121 @@
 <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>
+    <modelVersion>4.0.0</modelVersion>
 
-  <groupId>org.apache.maven.plugins.surefire</groupId>
-  <artifactId>junit4</artifactId>
-  <version>1.0-SNAPSHOT</version>
-  <name>Test for JUnit 4</name>
+    <groupId>org.apache.maven.plugins.surefire</groupId>
+    <artifactId>junit4</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <name>Test for JUnit 4</name>
 
-  <properties>
-    <junitVersion>4.4</junitVersion>
-    <maven.compiler.source>1.6</maven.compiler.source>
-    <maven.compiler.target>1.6</maven.compiler.target>
-  </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
 
-  <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>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>junit4</id>
+            <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>
+        </profile>
+
+        <profile>
+            <id>junit5-jupiter</id>
+            <properties>
+                <maven.compiler.source>1.8</maven.compiler.source>
+                <maven.compiler.target>1.8</maven.compiler.target>
+                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+                <junit.jupiter.version>5.1.1</junit.jupiter.version>
+                <junit.vintage.version>5.1.1</junit.vintage.version>
+                <junit.platform.version>1.1.1</junit.platform.version>
+            </properties>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <version>${surefire.version}</version>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.junit.platform</groupId>
+                                <artifactId>junit-platform-surefire-provider</artifactId>
+                                <version>${junit.platform.version}</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.junit.jupiter</groupId>
+                                <artifactId>junit-jupiter-engine</artifactId>
+                                <version>${junit.jupiter.version}</version>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>junit5-vintage</id>
+            <properties>
+                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+                <junit.platform.version>1.1.1</junit.platform.version>
+            </properties>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <version>${surefire.version}</version>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.junit.platform</groupId>
+                                <artifactId>junit-platform-surefire-provider</artifactId>
+                                <version>${junit.platform.version}</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.junit.vintage</groupId>
+                                <artifactId>junit-vintage-engine</artifactId>
+                                <version>${junit.version}</version>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 
 </project>
diff --git a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
index e9234f2..144df74 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
@@ -25,14 +25,12 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
-
 public class BasicTest
 {
+    private static boolean tearDownCalled = false;
 
     private boolean setUpCalled = false;
 
-    private static boolean tearDownCalled = false;
-    
     @Before
     public void setUp()
     {
diff --git a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/resources/junit4/src/test/java/junit5/JUnit5Test.java
similarity index 66%
copy from surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
copy to surefire-its/src/test/resources/junit4/src/test/java/junit5/JUnit5Test.java
index e9234f2..0dd000c 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/resources/junit4/src/test/java/junit5/JUnit5Test.java
@@ -1,4 +1,4 @@
-package junit4;
+package junit5;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,32 +19,32 @@ package junit4;
  * under the License.
  */
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public class BasicTest
+/**
+ * A test using the JUnit 5 API, which should be executed by JUnit jupiter enigne
+ */
+public class JUnit5Test
 {
+    private static boolean tearDownCalled;
 
-    private boolean setUpCalled = false;
+    private boolean setUpCalled;
 
-    private static boolean tearDownCalled = false;
-    
-    @Before
+    @BeforeEach
     public void setUp()
     {
         setUpCalled = true;
-        tearDownCalled = false;
         System.out.println( "Called setUp" );
     }
 
-    @After
+    @AfterEach
     public void tearDown()
     {
-        setUpCalled = false;
         tearDownCalled = true;
         System.out.println( "Called tearDown" );
     }
@@ -52,14 +52,13 @@ public class BasicTest
     @Test
     public void testSetUp()
     {
-        Assert.assertTrue( "setUp was not called", setUpCalled );
+        assertTrue( setUpCalled, "setUp was not called" );
     }
-  
 
-    @AfterClass
+    @AfterAll
     public static void oneTimeTearDown()
     {
-        
+        assertTrue( tearDownCalled, "tearDown was not called" );
     }
 
 }
diff --git a/surefire-its/src/test/resources/junit5/pom.xml b/surefire-its/src/test/resources/junit5/pom.xml
new file mode 100644
index 0000000..b44c2cc
--- /dev/null
+++ b/surefire-its/src/test/resources/junit5/pom.xml
@@ -0,0 +1,77 @@
+<?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/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.maven.plugins.surefire</groupId>
+    <artifactId>junit5</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <name>Test for JUnit 5</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java.version>1.8</java.version>
+        <junit.jupiter.version>5.1.1</junit.jupiter.version>
+        <junit.vintage.version>5.1.1</junit.vintage.version>
+        <junit.platform.version>1.1.1</junit.platform.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.vintage</groupId>
+            <artifactId>junit-vintage-engine</artifactId>
+            <version>${junit.vintage.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${surefire.version}</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.junit.platform</groupId>
+                        <artifactId>junit-platform-surefire-provider</artifactId>
+                        <version>${junit.platform.version}</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/resources/junit5/src/test/java/junit5/JUnit4Test.java
similarity index 83%
copy from surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
copy to surefire-its/src/test/resources/junit5/src/test/java/junit5/JUnit4Test.java
index e9234f2..0a94b1a 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/resources/junit5/src/test/java/junit5/JUnit4Test.java
@@ -1,4 +1,4 @@
-package junit4;
+package junit5;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -25,26 +25,26 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
-
-public class BasicTest
+/**
+ * A test using the JUnit 4 API, which should be executed by JUnit vintage enigne
+ */
+public class JUnit4Test
 {
 
-    private boolean setUpCalled = false;
+    private boolean setUpCalled;
+
+    private static boolean tearDownCalled;
 
-    private static boolean tearDownCalled = false;
-    
     @Before
     public void setUp()
     {
         setUpCalled = true;
-        tearDownCalled = false;
         System.out.println( "Called setUp" );
     }
 
     @After
     public void tearDown()
     {
-        setUpCalled = false;
         tearDownCalled = true;
         System.out.println( "Called tearDown" );
     }
@@ -54,12 +54,11 @@ public class BasicTest
     {
         Assert.assertTrue( "setUp was not called", setUpCalled );
     }
-  
 
     @AfterClass
     public static void oneTimeTearDown()
     {
-        
+        Assert.assertTrue( "tearDown was not called", tearDownCalled );
     }
 
 }
diff --git a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java b/surefire-its/src/test/resources/junit5/src/test/java/junit5/JUnit5Test.java
similarity index 66%
copy from surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
copy to surefire-its/src/test/resources/junit5/src/test/java/junit5/JUnit5Test.java
index e9234f2..558546d 100644
--- a/surefire-its/src/test/resources/junit4/src/test/java/junit4/BasicTest.java
+++ b/surefire-its/src/test/resources/junit5/src/test/java/junit5/JUnit5Test.java
@@ -1,4 +1,4 @@
-package junit4;
+package junit5;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,32 +19,33 @@ package junit4;
  * under the License.
  */
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 
-public class BasicTest
+/**
+ * A test using the JUnit 5 API, which should be executed by JUnit jupiter enigne
+ */
+public class JUnit5Test
 {
 
-    private boolean setUpCalled = false;
+    private boolean setUpCalled;
+
+    private static boolean tearDownCalled;
 
-    private static boolean tearDownCalled = false;
-    
-    @Before
+    @BeforeEach
     public void setUp()
     {
         setUpCalled = true;
-        tearDownCalled = false;
         System.out.println( "Called setUp" );
     }
 
-    @After
+    @AfterEach
     public void tearDown()
     {
-        setUpCalled = false;
         tearDownCalled = true;
         System.out.println( "Called tearDown" );
     }
@@ -52,14 +53,13 @@ public class BasicTest
     @Test
     public void testSetUp()
     {
-        Assert.assertTrue( "setUp was not called", setUpCalled );
+        Assertions.assertTrue( setUpCalled, "setUp was not called" );
     }
-  
 
-    @AfterClass
+    @AfterAll
     public static void oneTimeTearDown()
     {
-        
+        Assertions.assertTrue( tearDownCalled, "tearDown was not called" );
     }
 
 }
diff --git a/surefire-providers/pom.xml b/surefire-providers/pom.xml
index 0322bcc..47b0f40 100644
--- a/surefire-providers/pom.xml
+++ b/surefire-providers/pom.xml
@@ -41,6 +41,7 @@
     <module>surefire-junit3</module>
     <module>surefire-junit4</module>
     <module>surefire-junit47</module>
+    <module>surefire-junit-platform</module>
     <module>surefire-testng-utils</module>
     <module>surefire-testng</module>
   </modules>
diff --git a/surefire-providers/surefire-junit-platform/pom.xml b/surefire-providers/surefire-junit-platform/pom.xml
new file mode 100644
index 0000000..bf4869b
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/pom.xml
@@ -0,0 +1,161 @@
+<!--
+  ~ 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>
+
+    <parent>
+        <groupId>org.apache.maven.surefire</groupId>
+        <artifactId>surefire-providers</artifactId>
+        <version>3.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>surefire-junit-platform</artifactId>
+    <name>SureFire JUnit Platform Runner</name>
+    <description>SureFire JUnit Platform Runner</description>
+    <properties>
+        <javaVersion>8</javaVersion>
+    </properties>
+    <contributors>
+        <contributor>
+            <name>Konstantin Lutovich</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+        <contributor>
+            <name>Shintaro Katafuchi</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+        <contributor>
+            <name>Sam Brannen</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+        <contributor>
+            <name>Stefan Bechtold</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+        <contributor>
+            <name>Marc Philipp</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+        <contributor>
+            <name>Matthias Merdes</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+        <contributor>
+            <name>Johannes Link</name>
+            <roles>
+                <role>Contributed to the original provider implementation</role>
+            </roles>
+        </contributor>
+    </contributors>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.maven.surefire</groupId>
+            <artifactId>common-java5</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-launcher</artifactId>
+            <version>1.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.2.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>3.6.0</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>animal-sniffer-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>signature-check</id>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                        <configuration>
+                            <signature combine.self="override">
+                                <groupId>org.codehaus.mojo.signature</groupId>
+                                <artifactId>java18</artifactId>
+                                <version>1.0</version>
+                            </signature>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <jvm>${java.home}/bin/java</jvm>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <artifactSet>
+                                <includes>
+                                    <include>org.apache.maven.surefire:common-java5</include>
+                                </includes>
+                            </artifactSet>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
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
new file mode 100644
index 0000000..510266e
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProvider.java
@@ -0,0 +1,241 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static java.util.Collections.emptyMap;
+import static java.util.stream.Collectors.toList;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.maven.surefire.providerapi.AbstractProvider;
+import org.apache.maven.surefire.providerapi.ProviderParameters;
+import org.apache.maven.surefire.report.ConsoleOutputCapture;
+import org.apache.maven.surefire.report.ConsoleOutputReceiver;
+import org.apache.maven.surefire.report.ReporterException;
+import org.apache.maven.surefire.report.ReporterFactory;
+import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.suite.RunResult;
+import org.apache.maven.surefire.testset.TestListResolver;
+import org.apache.maven.surefire.testset.TestSetFailedException;
+import org.apache.maven.surefire.util.ScanResult;
+import org.apache.maven.surefire.util.TestsToRun;
+import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.commons.util.StringUtils;
+import org.junit.platform.engine.Filter;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TagFilter;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+public class JUnitPlatformProvider
+    extends AbstractProvider
+{
+
+    // Parameter names processed to determine which @Tags should be executed.
+    static final String EXCLUDE_GROUPS = "excludedGroups";
+
+    static final String EXCLUDE_TAGS = "excludeTags";
+
+    static final String INCLUDE_GROUPS = "groups";
+
+    static final String INCLUDE_TAGS = "includeTags";
+
+    static final String CONFIGURATION_PARAMETERS = "configurationParameters";
+
+    static final String EXCEPTION_MESSAGE_BOTH_NOT_ALLOWED = "The " + INCLUDE_GROUPS + " and " + INCLUDE_TAGS
+            + " parameters (or the " + EXCLUDE_GROUPS + " and " + EXCLUDE_TAGS + " parameters) are synonyms - "
+            + "only one of each is allowed (though neither is required).";
+
+    private final ProviderParameters parameters;
+
+    private final Launcher launcher;
+
+    final Filter<?>[] filters;
+
+    final Map<String, String> configurationParameters;
+
+    JUnitPlatformProvider( ProviderParameters parameters )
+    {
+        this( parameters, LauncherFactory.create() );
+    }
+
+    JUnitPlatformProvider( ProviderParameters parameters, Launcher launcher )
+    {
+        this.parameters = parameters;
+        this.launcher = launcher;
+        this.filters = getFilters();
+        this.configurationParameters = getConfigurationParameters();
+        Logger.getLogger( "org.junit" ).setLevel( Level.WARNING );
+    }
+
+    @Override
+    public Iterable<Class<?>> getSuites()
+    {
+        return scanClasspath();
+    }
+
+    @Override
+    public RunResult invoke( Object forkTestSet )
+                    throws TestSetFailedException, ReporterException
+    {
+        if ( forkTestSet instanceof TestsToRun )
+        {
+            return invokeAllTests( (TestsToRun) forkTestSet );
+        }
+        else if ( forkTestSet instanceof Class )
+        {
+            return invokeAllTests( TestsToRun.fromClass( (Class<?>) forkTestSet ) );
+        }
+        else if ( forkTestSet == null )
+        {
+            return invokeAllTests( scanClasspath() );
+        }
+        else
+        {
+            throw new IllegalArgumentException( "Unexpected value of forkTestSet: " + forkTestSet );
+        }
+    }
+
+    private TestsToRun scanClasspath()
+    {
+        TestPlanScannerFilter filter = new TestPlanScannerFilter( launcher, filters );
+        ScanResult scanResult = parameters.getScanResult();
+        TestsToRun scannedClasses = scanResult.applyFilter( filter, parameters.getTestClassLoader() );
+        return parameters.getRunOrderCalculator().orderTestClasses( scannedClasses );
+    }
+
+    private RunResult invokeAllTests( TestsToRun testsToRun )
+    {
+        RunResult runResult;
+        ReporterFactory reporterFactory = parameters.getReporterFactory();
+        try
+        {
+            RunListener runListener = reporterFactory.createReporter();
+            ConsoleOutputCapture.startCapture( (ConsoleOutputReceiver) runListener );
+            LauncherDiscoveryRequest discoveryRequest = buildLauncherDiscoveryRequest( testsToRun );
+            launcher.execute( discoveryRequest, new RunListenerAdapter( runListener ) );
+        }
+        finally
+        {
+            runResult = reporterFactory.close();
+        }
+        return runResult;
+    }
+
+    private LauncherDiscoveryRequest buildLauncherDiscoveryRequest( TestsToRun testsToRun )
+    {
+        LauncherDiscoveryRequestBuilder builder =
+                        request().filters( filters ).configurationParameters( configurationParameters );
+        for ( Class<?> testClass : testsToRun )
+        {
+            builder.selectors( selectClass( testClass ) );
+        }
+        return builder.build();
+    }
+
+    private Filter<?>[] getFilters()
+    {
+        List<Filter<?>> filters = new ArrayList<>();
+
+        Optional<List<String>> includes =
+                        getGroupsOrTags( getPropertiesList( INCLUDE_GROUPS ), getPropertiesList( INCLUDE_TAGS ) );
+        includes.map( TagFilter::includeTags ).ifPresent( filters::add );
+
+        Optional<List<String>> excludes =
+                        getGroupsOrTags( getPropertiesList( EXCLUDE_GROUPS ), getPropertiesList( EXCLUDE_TAGS ) );
+        excludes.map( TagFilter::excludeTags ).ifPresent( filters::add );
+
+        TestListResolver testListResolver = parameters.getTestRequest().getTestListResolver();
+        if ( !testListResolver.isEmpty() )
+        {
+            filters.add( new TestMethodFilter( testListResolver ) );
+        }
+
+        return filters.toArray( new Filter<?>[0] );
+    }
+
+    private Map<String, String> getConfigurationParameters()
+    {
+        String content = parameters.getProviderProperties().get( CONFIGURATION_PARAMETERS );
+        if ( content == null )
+        {
+            return emptyMap();
+        }
+        try ( StringReader reader = new StringReader( content ) )
+        {
+            Map<String, String> result = new HashMap<>();
+            Properties props = new Properties();
+            props.load( reader );
+            props.stringPropertyNames().forEach( key -> result.put( key, props.getProperty( key ) ) );
+            return result;
+        }
+        catch ( IOException ex )
+        {
+            throw new UncheckedIOException( "Error reading " + CONFIGURATION_PARAMETERS, ex );
+        }
+    }
+
+    private Optional<List<String>> getPropertiesList( String key )
+    {
+        List<String> compoundProperties = null;
+        String property = parameters.getProviderProperties().get( key );
+        if ( StringUtils.isNotBlank( property ) )
+        {
+            compoundProperties =
+                            Arrays.stream( property.split( "[,]+" ) )
+                                  .filter( StringUtils::isNotBlank )
+                                  .map( String::trim )
+                                  .collect( toList() );
+        }
+        return Optional.ofNullable( compoundProperties );
+    }
+
+    private Optional<List<String>> getGroupsOrTags( Optional<List<String>> groups, Optional<List<String>> tags )
+    {
+        Optional<List<String>> elements = Optional.empty();
+
+        Preconditions.condition(!groups.isPresent() || !tags.isPresent(), EXCEPTION_MESSAGE_BOTH_NOT_ALLOWED );
+
+        if ( groups.isPresent() )
+        {
+            elements = groups;
+        }
+        else if ( tags.isPresent() )
+        {
+            elements = tags;
+        }
+
+        return elements;
+    }
+}
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
new file mode 100644
index 0000000..71a70c3
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/RunListenerAdapter.java
@@ -0,0 +1,272 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static org.apache.maven.surefire.report.SimpleReportEntry.ignored;
+import static org.junit.platform.engine.TestExecutionResult.Status.ABORTED;
+import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.maven.surefire.report.PojoStackTraceWriter;
+import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.report.SimpleReportEntry;
+import org.apache.maven.surefire.report.StackTraceWriter;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.listeners.LegacyReportingUtils;
+
+/**
+ * @since 1.0
+ */
+final class RunListenerAdapter
+    implements TestExecutionListener
+{
+
+    private final RunListener runListener;
+
+    private TestPlan testPlan;
+
+    private Set<TestIdentifier> testSetNodes = ConcurrentHashMap.newKeySet();
+
+    RunListenerAdapter( RunListener runListener )
+    {
+        this.runListener = runListener;
+    }
+
+    @Override
+    public void testPlanExecutionStarted( TestPlan testPlan )
+    {
+        updateTestPlan( testPlan );
+    }
+
+    @Override
+    public void testPlanExecutionFinished( TestPlan testPlan )
+    {
+        updateTestPlan( null );
+    }
+
+    @Override
+    public void executionStarted( TestIdentifier testIdentifier )
+    {
+        if ( testIdentifier.isContainer()
+                        && testIdentifier.getSource().filter( ClassSource.class::isInstance ).isPresent() )
+        {
+            startTestSetIfPossible( testIdentifier );
+        }
+        if ( testIdentifier.isTest() )
+        {
+            ensureTestSetStarted( testIdentifier );
+            runListener.testStarting( createReportEntry( testIdentifier ) );
+        }
+    }
+
+    @Override
+    public void executionSkipped( TestIdentifier testIdentifier, String reason )
+    {
+        ensureTestSetStarted( testIdentifier );
+        String source = getLegacyReportingClassName( testIdentifier );
+        runListener.testSkipped( ignored( source, getLegacyReportingName( testIdentifier ), reason ) );
+        completeTestSetIfNecessary( testIdentifier );
+    }
+
+    @Override
+    public void executionFinished(
+                    TestIdentifier testIdentifier, TestExecutionResult testExecutionResult )
+    {
+        if ( testExecutionResult.getStatus() == ABORTED )
+        {
+            runListener.testAssumptionFailure( createReportEntry( testIdentifier, testExecutionResult ) );
+        }
+        else if ( testExecutionResult.getStatus() == FAILED )
+        {
+            reportFailedTest( testIdentifier, testExecutionResult );
+        }
+        else if ( testIdentifier.isTest() )
+        {
+            runListener.testSucceeded( createReportEntry( testIdentifier ) );
+        }
+        completeTestSetIfNecessary( testIdentifier );
+    }
+
+    private void updateTestPlan( TestPlan testPlan )
+    {
+        this.testPlan = testPlan;
+        testSetNodes.clear();
+    }
+
+    private void ensureTestSetStarted( TestIdentifier testIdentifier )
+    {
+        if ( isTestSetStarted( testIdentifier ) )
+        {
+            return;
+        }
+        if ( testIdentifier.isTest() )
+        {
+            startTestSet( testPlan.getParent( testIdentifier ).orElse( testIdentifier ) );
+        }
+        else
+        {
+            startTestSet( testIdentifier );
+        }
+    }
+
+    private boolean isTestSetStarted( TestIdentifier testIdentifier )
+    {
+        return testSetNodes.contains( testIdentifier )
+                        || testPlan.getParent( testIdentifier ).map( this::isTestSetStarted ).orElse( false );
+    }
+
+    private void startTestSetIfPossible( TestIdentifier testIdentifier )
+    {
+        if ( !isTestSetStarted( testIdentifier ) )
+        {
+            startTestSet( testIdentifier );
+        }
+    }
+
+    private void completeTestSetIfNecessary( TestIdentifier testIdentifier )
+    {
+        if ( testSetNodes.contains( testIdentifier ) )
+        {
+            completeTestSet( testIdentifier );
+        }
+    }
+
+    private void startTestSet( TestIdentifier testIdentifier )
+    {
+        runListener.testSetStarting( createTestSetReportEntry( testIdentifier ) );
+        testSetNodes.add( testIdentifier );
+    }
+
+    private void completeTestSet( TestIdentifier testIdentifier )
+    {
+        runListener.testSetCompleted( createTestSetReportEntry( testIdentifier ) );
+        testSetNodes.remove( testIdentifier );
+    }
+
+    private void reportFailedTest(
+                    TestIdentifier testIdentifier, TestExecutionResult testExecutionResult )
+    {
+        SimpleReportEntry reportEntry = createReportEntry( testIdentifier, testExecutionResult );
+        if ( testExecutionResult.getThrowable().filter( AssertionError.class::isInstance ).isPresent() )
+        {
+            runListener.testFailed( reportEntry );
+        }
+        else
+        {
+            runListener.testError( reportEntry );
+        }
+    }
+
+    private SimpleReportEntry createTestSetReportEntry( TestIdentifier testIdentifier )
+    {
+        return new SimpleReportEntry(
+                        JUnitPlatformProvider.class.getName(), testIdentifier.getLegacyReportingName() );
+    }
+
+    private SimpleReportEntry createReportEntry( TestIdentifier testIdentifier )
+    {
+        return createReportEntry( testIdentifier, (StackTraceWriter) null );
+    }
+
+    private SimpleReportEntry createReportEntry(
+                    TestIdentifier testIdentifier, TestExecutionResult testExecutionResult )
+    {
+        return createReportEntry(
+                        testIdentifier, getStackTraceWriter( testIdentifier, testExecutionResult ) );
+    }
+
+    private SimpleReportEntry createReportEntry(
+                    TestIdentifier testIdentifier, StackTraceWriter stackTraceWriter )
+    {
+        String source = getLegacyReportingClassName( testIdentifier );
+        String name = getLegacyReportingName( testIdentifier );
+
+        return SimpleReportEntry.withException( source, name, stackTraceWriter );
+    }
+
+    private String getLegacyReportingName( TestIdentifier testIdentifier )
+    {
+        // Surefire cuts off the name at the first '(' character. Thus, we have to pick a different
+        // character to represent parentheses. "()" are removed entirely to maximize compatibility with
+        // existing reporting tools because in the old days test methods used to not have parameters.
+        return testIdentifier
+                        .getLegacyReportingName()
+                        .replace( "()", "" )
+                        .replace( '(', '{' )
+                        .replace( ')', '}' );
+    }
+
+    private String getLegacyReportingClassName( TestIdentifier testIdentifier )
+    {
+        return LegacyReportingUtils.getClassName( testPlan, testIdentifier );
+    }
+
+    private StackTraceWriter getStackTraceWriter(
+                    TestIdentifier testIdentifier, TestExecutionResult testExecutionResult )
+    {
+        Optional<Throwable> throwable = testExecutionResult.getThrowable();
+        if ( testExecutionResult.getStatus() == FAILED )
+        {
+            // Failed tests must have a StackTraceWriter, otherwise Surefire will fail
+            return getStackTraceWriter( testIdentifier, throwable.orElse( null ) );
+        }
+        return throwable.map( t -> getStackTraceWriter( testIdentifier, t ) ).orElse( null );
+    }
+
+    private StackTraceWriter getStackTraceWriter( TestIdentifier testIdentifier, Throwable throwable )
+    {
+        String className = getClassName( testIdentifier );
+        String methodName = getMethodName( testIdentifier ).orElse( "" );
+        return new PojoStackTraceWriter( className, methodName, throwable );
+    }
+
+    private String getClassName( TestIdentifier testIdentifier )
+    {
+        TestSource testSource = testIdentifier.getSource().orElse( null );
+        if ( testSource instanceof ClassSource )
+        {
+            return ( (ClassSource) testSource ).getJavaClass().getName();
+        }
+        if ( testSource instanceof MethodSource )
+        {
+            return ( (MethodSource) testSource ).getClassName();
+        }
+        return testPlan.getParent( testIdentifier ).map( this::getClassName ).orElse( "" );
+    }
+
+    private Optional<String> getMethodName( TestIdentifier testIdentifier )
+    {
+        TestSource testSource = testIdentifier.getSource().orElse( null );
+        if ( testSource instanceof MethodSource )
+        {
+            return Optional.of( ( (MethodSource) testSource ).getMethodName() );
+        }
+        return Optional.empty();
+    }
+}
diff --git a/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/TestMethodFilter.java b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/TestMethodFilter.java
new file mode 100644
index 0000000..45e32db
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/TestMethodFilter.java
@@ -0,0 +1,60 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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.surefire.testset.TestListResolver;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.launcher.PostDiscoveryFilter;
+
+/**
+ * @since 1.0.3
+ */
+class TestMethodFilter
+    implements PostDiscoveryFilter
+{
+
+    private final TestListResolver testListResolver;
+
+    TestMethodFilter( TestListResolver testListResolver )
+    {
+        this.testListResolver = testListResolver;
+    }
+
+    @Override
+    public FilterResult apply( TestDescriptor descriptor )
+    {
+        boolean shouldRun = descriptor.getSource()
+                                      .filter( MethodSource.class::isInstance )
+                                      .map( MethodSource.class::cast )
+                                      .map( this::shouldRun )
+                                      .orElse( true );
+
+        return FilterResult.includedIf( shouldRun );
+    }
+
+    private boolean shouldRun( MethodSource source )
+    {
+        String testClass = TestListResolver.toClassFileName( source.getClassName() );
+        String testMethod = source.getMethodName();
+        return this.testListResolver.shouldRun( testClass, testMethod );
+    }
+}
diff --git a/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/TestPlanScannerFilter.java b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/TestPlanScannerFilter.java
new file mode 100644
index 0000000..4b488c1
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/TestPlanScannerFilter.java
@@ -0,0 +1,60 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
+
+import org.apache.maven.surefire.util.ScannerFilter;
+import org.junit.platform.engine.Filter;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestPlan;
+
+/**
+ * @since 1.0
+ */
+final class TestPlanScannerFilter
+    implements ScannerFilter
+{
+
+    private final Launcher launcher;
+
+    private final Filter<?>[] includeAndExcludeFilters;
+
+    TestPlanScannerFilter( Launcher launcher, Filter<?>[] includeAndExcludeFilters )
+    {
+        this.launcher = launcher;
+        this.includeAndExcludeFilters = includeAndExcludeFilters;
+    }
+
+    @Override
+    @SuppressWarnings( "rawtypes" )
+    public boolean accept( Class testClass )
+    {
+        LauncherDiscoveryRequest discoveryRequest = request()
+                        .selectors( selectClass( testClass ) )
+                        .filters( includeAndExcludeFilters ).build();
+
+        TestPlan testPlan = launcher.discover( discoveryRequest );
+
+        return testPlan.containsTests();
+    }
+}
diff --git a/surefire-providers/surefire-junit-platform/src/main/resources/META-INF/services/org.apache.maven.surefire.providerapi.SurefireProvider b/surefire-providers/surefire-junit-platform/src/main/resources/META-INF/services/org.apache.maven.surefire.providerapi.SurefireProvider
new file mode 100644
index 0000000..dbe73cf
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/main/resources/META-INF/services/org.apache.maven.surefire.providerapi.SurefireProvider
@@ -0,0 +1 @@
+org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTests.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTests.java
new file mode 100644
index 0000000..e0db97e
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTests.java
@@ -0,0 +1,756 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static java.util.stream.Collectors.toSet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.mockito.AdditionalMatchers.gt;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.surefire.providerapi.ProviderParameters;
+import org.apache.maven.surefire.report.ConsoleOutputReceiver;
+import org.apache.maven.surefire.report.ReportEntry;
+import org.apache.maven.surefire.report.ReporterFactory;
+import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.report.SimpleReportEntry;
+import org.apache.maven.surefire.testset.TestListResolver;
+import org.apache.maven.surefire.testset.TestRequest;
+import org.apache.maven.surefire.testset.TestSetFailedException;
+import org.apache.maven.surefire.util.RunOrderCalculator;
+import org.apache.maven.surefire.util.ScanResult;
+import org.apache.maven.surefire.util.TestsToRun;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.PreconditionViolationException;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.junit.platform.launcher.listeners.TestExecutionSummary.Failure;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+/**
+ * Unit tests for {@link JUnitPlatformProvider}.
+ *
+ * @since 1.0
+ */
+class JUnitPlatformProviderTests
+{
+
+    @Test
+    void getSuitesReturnsScannedClasses()
+                    throws Exception
+    {
+        ProviderParameters providerParameters =
+                        providerParametersMock( TestClass1.class, TestClass2.class );
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertThat( provider.getSuites() ).containsOnly( TestClass1.class, TestClass2.class );
+    }
+
+    @Test
+    void invokeThrowsForWrongForkTestSet()
+                    throws Exception
+    {
+        ProviderParameters providerParameters = providerParametersMock( Integer.class );
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertThrows(
+                        IllegalArgumentException.class, () -> invokeProvider( provider, "wrong forkTestSet" ) );
+    }
+
+    @Test
+    void allGivenTestsToRunAreInvoked()
+                    throws Exception
+    {
+        Launcher launcher = LauncherFactory.create();
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParametersMock(), launcher );
+
+        TestPlanSummaryListener executionListener = new TestPlanSummaryListener();
+        launcher.registerTestExecutionListeners( executionListener );
+
+        TestsToRun testsToRun = newTestsToRun( TestClass1.class, TestClass2.class );
+        invokeProvider( provider, testsToRun );
+
+        assertThat( executionListener.summaries ).hasSize( 1 );
+        TestExecutionSummary summary = executionListener.summaries.get( 0 );
+        assertEquals( TestClass1.TESTS_FOUND + TestClass2.TESTS_FOUND, summary.getTestsFoundCount() );
+        assertEquals(
+                        TestClass1.TESTS_STARTED + TestClass2.TESTS_STARTED, summary.getTestsStartedCount() );
+        assertEquals(
+                        TestClass1.TESTS_SKIPPED + TestClass2.TESTS_SKIPPED, summary.getTestsSkippedCount() );
+        assertEquals(
+                        TestClass1.TESTS_SUCCEEDED + TestClass2.TESTS_SUCCEEDED, summary.getTestsSucceededCount() );
+        assertEquals(
+                        TestClass1.TESTS_ABORTED + TestClass2.TESTS_ABORTED, summary.getTestsAbortedCount() );
+        assertEquals( TestClass1.TESTS_FAILED + TestClass2.TESTS_FAILED, summary.getTestsFailedCount() );
+    }
+
+    @Test
+    void singleTestClassIsInvoked()
+                    throws Exception
+    {
+        Launcher launcher = LauncherFactory.create();
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParametersMock(), launcher );
+
+        TestPlanSummaryListener executionListener = new TestPlanSummaryListener();
+        launcher.registerTestExecutionListeners( executionListener );
+
+        invokeProvider( provider, TestClass1.class );
+
+        assertThat( executionListener.summaries ).hasSize( 1 );
+        TestExecutionSummary summary = executionListener.summaries.get( 0 );
+        assertEquals( TestClass1.TESTS_FOUND, summary.getTestsFoundCount() );
+        assertEquals( TestClass1.TESTS_STARTED, summary.getTestsStartedCount() );
+        assertEquals( TestClass1.TESTS_SKIPPED, summary.getTestsSkippedCount() );
+        assertEquals( TestClass1.TESTS_SUCCEEDED, summary.getTestsSucceededCount() );
+        assertEquals( TestClass1.TESTS_ABORTED, summary.getTestsAbortedCount() );
+        assertEquals( TestClass1.TESTS_FAILED, summary.getTestsFailedCount() );
+    }
+
+    @Test
+    void allDiscoveredTestsAreInvokedForNullArgument()
+                    throws Exception
+    {
+        RunListener runListener = runListenerMock();
+        ProviderParameters providerParameters =
+                        providerParametersMock( runListener, TestClass1.class, TestClass2.class );
+        Launcher launcher = LauncherFactory.create();
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters, launcher );
+
+        TestPlanSummaryListener executionListener = new TestPlanSummaryListener();
+        launcher.registerTestExecutionListeners( executionListener );
+
+        invokeProvider( provider, null );
+
+        InOrder inOrder = inOrder( runListener );
+        inOrder
+                        .verify( runListener )
+                        .testSetStarting(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        TestClass1.class.getName() ) );
+        inOrder
+                        .verify( runListener )
+                        .testSetCompleted(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        TestClass1.class.getName() ) );
+        inOrder
+                        .verify( runListener )
+                        .testSetStarting(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        TestClass2.class.getName() ) );
+        inOrder
+                        .verify( runListener )
+                        .testSetCompleted(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        TestClass2.class.getName() ) );
+
+        assertThat( executionListener.summaries ).hasSize( 1 );
+        TestExecutionSummary summary = executionListener.summaries.get( 0 );
+        assertEquals( TestClass1.TESTS_FOUND + TestClass2.TESTS_FOUND, summary.getTestsFoundCount() );
+        assertEquals(
+                        TestClass1.TESTS_STARTED + TestClass2.TESTS_STARTED, summary.getTestsStartedCount() );
+        assertEquals(
+                        TestClass1.TESTS_SKIPPED + TestClass2.TESTS_SKIPPED, summary.getTestsSkippedCount() );
+        assertEquals(
+                        TestClass1.TESTS_SUCCEEDED + TestClass2.TESTS_SUCCEEDED, summary.getTestsSucceededCount() );
+        assertEquals(
+                        TestClass1.TESTS_ABORTED + TestClass2.TESTS_ABORTED, summary.getTestsAbortedCount() );
+        assertEquals( TestClass1.TESTS_FAILED + TestClass2.TESTS_FAILED, summary.getTestsFailedCount() );
+    }
+
+    @Test
+    void outputIsCaptured()
+                    throws Exception
+    {
+        Launcher launcher = LauncherFactory.create();
+        RunListener runListener = runListenerMock();
+        JUnitPlatformProvider provider =
+                        new JUnitPlatformProvider( providerParametersMock( runListener ), launcher );
+
+        invokeProvider( provider, VerboseTestClass.class );
+
+        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass( byte[].class );
+        // @formatter:off
+        verify( (ConsoleOutputReceiver) runListener )
+                        .writeTestOutput( captor.capture(), eq( 0 ), gt( 6 ), eq( true ) );
+        verify( (ConsoleOutputReceiver) runListener )
+                        .writeTestOutput( captor.capture(), eq( 0 ), gt( 6 ), eq( false ) );
+        assertThat( captor.getAllValues() )
+                        .extracting( bytes -> new String( bytes, 0, 6 ) )
+                        .containsExactly( "stdout", "stderr" );
+        // @formatter:on
+    }
+
+    @Test
+    void bothGroupsAndIncludeTagsThrowsException()
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.INCLUDE_GROUPS, "groupOne, groupTwo" );
+        properties.put( JUnitPlatformProvider.INCLUDE_TAGS, "tagOne, tagTwo" );
+        verifyPreconditionViolationException( properties );
+    }
+
+    @Test
+    void bothExcludedGroupsAndExcludeTagsThrowsException()
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.EXCLUDE_GROUPS, "groupOne, groupTwo" );
+        properties.put( JUnitPlatformProvider.EXCLUDE_TAGS, "tagOne, tagTwo" );
+        verifyPreconditionViolationException( properties );
+    }
+
+    @Test
+    void onlyGroupsIsDeclared()
+                    throws Exception
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.INCLUDE_GROUPS, "groupOne, groupTwo" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 1, provider.filters.length );
+    }
+
+    @Test
+    void onlyExcludeTagsIsDeclared()
+                    throws Exception
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.EXCLUDE_TAGS, "tagOne, tagTwo" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 1, provider.filters.length );
+    }
+
+    @Test
+    void noFiltersAreCreatedIfTagsAreEmpty()
+                    throws Exception
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.INCLUDE_TAGS, "" );
+        properties.put( JUnitPlatformProvider.INCLUDE_GROUPS, "" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+        assertEquals( 0, provider.filters.length );
+    }
+
+    @Test
+    void filtersWithEmptyTagsAreNotRegistered()
+                    throws Exception
+    {
+        Map<String, String> properties = new HashMap<>();
+
+        // Here only tagOne is registered as a valid tag and other tags are ignored as they are empty
+        properties.put( JUnitPlatformProvider.EXCLUDE_GROUPS, "tagOne," );
+        properties.put( JUnitPlatformProvider.EXCLUDE_TAGS, "" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+        assertEquals( 1, provider.filters.length );
+    }
+
+    @Test
+    void bothIncludeAndExcludeAreAllowed()
+                    throws Exception
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.INCLUDE_TAGS, "tagOne, tagTwo" );
+        properties.put( JUnitPlatformProvider.EXCLUDE_TAGS, "tagThree, tagFour" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 2, provider.filters.length );
+    }
+
+    @Test
+    void tagExpressionsAreSupportedForIncludeTagsContainingVerticalBar()
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.INCLUDE_TAGS, "tagOne | tagTwo" );
+        properties.put( JUnitPlatformProvider.EXCLUDE_TAGS, "tagThree | tagFour" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 2, provider.filters.length );
+    }
+
+    @Test
+    void tagExpressionsAreSupportedForIncludeTagsContainingAmpersand()
+    {
+        Map<String, String> properties = new HashMap<>();
+        properties.put( JUnitPlatformProvider.INCLUDE_TAGS, "tagOne & !tagTwo" );
+        properties.put( JUnitPlatformProvider.EXCLUDE_TAGS, "tagThree & !tagFour" );
+
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 2, provider.filters.length );
+    }
+
+    @Test
+    void noFiltersAreCreatedIfNoPropertiesAreDeclared()
+                    throws Exception
+    {
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 0, provider.filters.length );
+    }
+
+    @Test
+    void defaultConfigurationParametersAreEmpty()
+    {
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( emptyMap() );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertTrue( provider.configurationParameters.isEmpty() );
+    }
+
+    @Test
+    void parsesConfigurationParameters()
+    {
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() )
+                        .thenReturn( //
+                                     singletonMap(
+                                                     JUnitPlatformProvider.CONFIGURATION_PARAMETERS,
+                                                     "foo = true\nbar 42\rbaz: *\r\nqux: EOF" ) );
+
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters );
+
+        assertEquals( 4, provider.configurationParameters.size() );
+        assertEquals( "true", provider.configurationParameters.get( "foo" ) );
+        assertEquals( "42", provider.configurationParameters.get( "bar" ) );
+        assertEquals( "*", provider.configurationParameters.get( "baz" ) );
+        assertEquals( "EOF", provider.configurationParameters.get( "qux" ) );
+    }
+
+    @Test
+    void executesSingleTestIncludedByName()
+                    throws Exception
+    {
+        // following is equivalent of adding '-Dtest=TestClass3#prefix1Suffix1'
+        // '*' needed because it's a nested class and thus has name prefixed with '$'
+        String pattern = "*TestClass3#prefix1Suffix1";
+
+        testExecutionOfMatchingTestMethods( TestClass3.class, pattern, "prefix1Suffix1()" );
+    }
+
+    @Test
+    void executesMultipleTestsIncludedByName()
+                    throws Exception
+    {
+        // following is equivalent of adding '-Dtest=TestClass3#prefix1Suffix1+prefix2Suffix1'
+        // '*' needed because it's a nested class and thus has name prefixed with '$'
+        String pattern = "*TestClass3#prefix1Suffix1+prefix2Suffix1";
+
+        testExecutionOfMatchingTestMethods(
+                        TestClass3.class, pattern, "prefix1Suffix1()", "prefix2Suffix1()" );
+    }
+
+    @Test
+    void executesMultipleTestsIncludedByNamePattern()
+                    throws Exception
+    {
+        // following is equivalent of adding '-Dtest=TestClass3#prefix1*'
+        // '*' needed because it's a nested class and thus has name prefixed with '$'
+        String pattern = "*TestClass3#prefix1*";
+
+        testExecutionOfMatchingTestMethods(
+                        TestClass3.class, pattern, "prefix1Suffix1()", "prefix1Suffix2()" );
+    }
+
+    @Test
+    void executesMultipleTestsIncludedByNamePatternWithQuestionMark()
+                    throws Exception
+    {
+        // following is equivalent of adding '-Dtest=TestClass3#prefix?Suffix2'
+        // '*' needed because it's a nested class and thus has name prefixed with '$'
+        String pattern = "*TestClass3#prefix?Suffix2";
+
+        testExecutionOfMatchingTestMethods(
+                        TestClass3.class, pattern, "prefix1Suffix2()", "prefix2Suffix2()" );
+    }
+
+    @Test
+    void doesNotExecuteTestsExcludedByName()
+                    throws Exception
+    {
+        // following is equivalent of adding '-Dtest=!TestClass3#prefix1Suffix2'
+        // '*' needed because it's a nested class and thus has name prefixed with '$'
+        String pattern = "!*TestClass3#prefix1Suffix2";
+
+        testExecutionOfMatchingTestMethods(
+                        TestClass3.class, pattern, "prefix1Suffix1()", "prefix2Suffix1()", "prefix2Suffix2()" );
+    }
+
+    @Test
+    void doesNotExecuteTestsExcludedByNamePattern()
+                    throws Exception
+    {
+        // following is equivalent of adding '-Dtest=!TestClass3#prefix2*'
+        // '*' needed because it's a nested class and thus has name prefixed with '$'
+        String pattern = "!*TestClass3#prefix2*";
+
+        testExecutionOfMatchingTestMethods(
+                        TestClass3.class, pattern, "prefix1Suffix1()", "prefix1Suffix2()" );
+    }
+
+    void testExecutionOfMatchingTestMethods(
+                    Class<?> testClass, String pattern, String... expectedTestNames )
+                    throws Exception
+    {
+        TestListResolver testListResolver = new TestListResolver( pattern );
+        ProviderParameters providerParameters = providerParametersMock( testListResolver, testClass );
+        Launcher launcher = LauncherFactory.create();
+        JUnitPlatformProvider provider = new JUnitPlatformProvider( providerParameters, launcher );
+
+        TestPlanSummaryListener executionListener = new TestPlanSummaryListener();
+        launcher.registerTestExecutionListeners( executionListener );
+
+        invokeProvider( provider, null );
+
+        assertEquals( 1, executionListener.summaries.size() );
+        TestExecutionSummary summary = executionListener.summaries.get( 0 );
+        int expectedCount = expectedTestNames.length;
+        assertEquals( expectedCount, summary.getTestsFoundCount() );
+        assertEquals( expectedCount, summary.getTestsFailedCount() );
+        assertEquals( expectedCount, summary.getFailures().size() );
+
+        assertThat( failedTestDisplayNames( summary ) ).contains( expectedTestNames );
+    }
+
+    private void verifyPreconditionViolationException( Map<String, String> properties )
+    {
+        ProviderParameters providerParameters = providerParametersMock( TestClass1.class );
+        when( providerParameters.getProviderProperties() ).thenReturn( properties );
+
+        Throwable throwable =
+                        assertThrows(
+                                        PreconditionViolationException.class,
+                                        () -> new JUnitPlatformProvider( providerParameters ) );
+
+        assertEquals( JUnitPlatformProvider.EXCEPTION_MESSAGE_BOTH_NOT_ALLOWED, throwable.getMessage() );
+    }
+
+    private static ProviderParameters providerParametersMock( Class<?>... testClasses )
+    {
+        return providerParametersMock( runListenerMock(), testClasses );
+    }
+
+    private static ProviderParameters providerParametersMock(
+                    RunListener runListener, Class<?>... testClasses )
+    {
+        TestListResolver testListResolver = new TestListResolver( "" );
+        return providerParametersMock( runListener, testListResolver, testClasses );
+    }
+
+    private static ProviderParameters providerParametersMock(
+                    TestListResolver testListResolver, Class<?>... testClasses )
+    {
+        return providerParametersMock( runListenerMock(), testListResolver, testClasses );
+    }
+
+    private static ProviderParameters providerParametersMock(
+                    RunListener runListener, TestListResolver testListResolver, Class<?>... testClasses )
+    {
+        TestsToRun testsToRun = newTestsToRun( testClasses );
+
+        ScanResult scanResult = mock( ScanResult.class );
+        when( scanResult.applyFilter( any(), any() ) ).thenReturn( testsToRun );
+
+        RunOrderCalculator runOrderCalculator = mock( RunOrderCalculator.class );
+        when( runOrderCalculator.orderTestClasses( any() ) ).thenReturn( testsToRun );
+
+        ReporterFactory reporterFactory = mock( ReporterFactory.class );
+        when( reporterFactory.createReporter() ).thenReturn( runListener );
+
+        TestRequest testRequest = mock( TestRequest.class );
+        when( testRequest.getTestListResolver() ).thenReturn( testListResolver );
+
+        ProviderParameters providerParameters = mock( ProviderParameters.class );
+        when( providerParameters.getScanResult() ).thenReturn( scanResult );
+        when( providerParameters.getRunOrderCalculator() ).thenReturn( runOrderCalculator );
+        when( providerParameters.getReporterFactory() ).thenReturn( reporterFactory );
+        when( providerParameters.getTestRequest() ).thenReturn( testRequest );
+
+        return providerParameters;
+    }
+
+    private static RunListener runListenerMock()
+    {
+        return mock( RunListener.class, withSettings().extraInterfaces( ConsoleOutputReceiver.class ) );
+    }
+
+    private static Set<String> failedTestDisplayNames( TestExecutionSummary summary )
+    {
+        // @formatter:off
+        return summary
+                        .getFailures()
+                        .stream()
+                        .map( Failure::getTestIdentifier )
+                        .map( TestIdentifier::getDisplayName )
+                        .collect( toSet() );
+        // @formatter:on
+    }
+
+    private static TestsToRun newTestsToRun( Class<?>... testClasses )
+    {
+        List<Class<?>> classesList = Arrays.asList( testClasses );
+        return new TestsToRun( new LinkedHashSet<>( classesList ) );
+    }
+
+    private class TestPlanSummaryListener
+                    extends SummaryGeneratingListener
+    {
+
+        final List<TestExecutionSummary> summaries = new ArrayList<>();
+
+        @Override
+        public void testPlanExecutionFinished( TestPlan testPlan )
+        {
+            super.testPlanExecutionFinished( testPlan );
+            summaries.add( getSummary() );
+        }
+    }
+
+    /**
+     * Invokes the provider, then restores system out and system error.
+     *
+     * @see <a href="https://github.com/junit-team/junit5/issues/986">#986</a>
+     */
+    private void invokeProvider( JUnitPlatformProvider provider, Object forkTestSet )
+                    throws TestSetFailedException, InvocationTargetException
+    {
+        PrintStream systemOut = System.out;
+        PrintStream systemErr = System.err;
+        try
+        {
+            provider.invoke( forkTestSet );
+        }
+        finally
+        {
+            System.setOut( systemOut );
+            System.setErr( systemErr );
+        }
+    }
+
+    static class TestClass1
+    {
+
+        static final int TESTS_FOUND = 4;
+
+        static final int TESTS_STARTED = 3;
+
+        static final int TESTS_SKIPPED = 1;
+
+        static final int TESTS_SUCCEEDED = 2;
+
+        static final int TESTS_ABORTED = 0;
+
+        static final int TESTS_FAILED = 1;
+
+        @Test
+        void test1()
+        {
+        }
+
+        @Test
+        void test2()
+        {
+        }
+
+        @Disabled
+        @Test
+        void test3()
+        {
+        }
+
+        @Test
+        void test4()
+        {
+            throw new RuntimeException();
+        }
+    }
+
+    static class TestClass2
+    {
+
+        static final int TESTS_FOUND = 3;
+
+        static final int TESTS_STARTED = 3;
+
+        static final int TESTS_SKIPPED = 0;
+
+        static final int TESTS_SUCCEEDED = 1;
+
+        static final int TESTS_ABORTED = 1;
+
+        static final int TESTS_FAILED = 1;
+
+        @Test
+        void test1()
+        {
+        }
+
+        @Test
+        void test2()
+        {
+            throw new RuntimeException();
+        }
+
+        @Test
+        void test3()
+        {
+            assumeTrue( false );
+        }
+    }
+
+    static class VerboseTestClass
+    {
+        @Test
+        void test()
+        {
+            System.out.println( "stdout" );
+            System.err.println( "stderr" );
+        }
+    }
+
+    @Test
+    void usesClassNamesForXmlReport()
+                    throws TestSetFailedException, InvocationTargetException
+    {
+        String[] classNames = { Sub1Tests.class.getName(), Sub2Tests.class.getName() };
+        ProviderParameters providerParameters =
+                        providerParametersMock( Sub1Tests.class, Sub2Tests.class );
+
+        JUnitPlatformProvider jUnitPlatformProvider = new JUnitPlatformProvider( providerParameters );
+        TestsToRun testsToRun = newTestsToRun( Sub1Tests.class, Sub2Tests.class );
+
+        invokeProvider( jUnitPlatformProvider, testsToRun );
+        RunListener reporter = providerParameters.getReporterFactory().createReporter();
+
+        ArgumentCaptor<ReportEntry> reportEntryArgumentCaptor =
+                        ArgumentCaptor.forClass( ReportEntry.class );
+        verify( reporter, times( 2 ) ).testSucceeded( reportEntryArgumentCaptor.capture() );
+
+        List<ReportEntry> allValues = reportEntryArgumentCaptor.getAllValues();
+        assertThat( allValues ).extracting( ReportEntry::getSourceName ).containsExactly( classNames );
+    }
+
+    static class AbstractTestClass
+    {
+        @Test
+        void test()
+        {
+        }
+    }
+
+    static class Sub1Tests
+                    extends AbstractTestClass
+    {
+    }
+
+    static class Sub2Tests
+                    extends AbstractTestClass
+    {
+    }
+
+    static class TestClass3
+    {
+        @Test
+        void prefix1Suffix1()
+        {
+            throw new RuntimeException();
+        }
+
+        @Test
+        void prefix2Suffix1()
+        {
+            throw new RuntimeException();
+        }
+
+        @Test
+        void prefix1Suffix2()
+        {
+            throw new RuntimeException();
+        }
+
+        @Test
+        void prefix2Suffix2()
+        {
+            throw new RuntimeException();
+        }
+    }
+}
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTests.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTests.java
new file mode 100644
index 0000000..d3ec7a4
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/RunListenerAdapterTests.java
@@ -0,0 +1,580 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.platform.engine.TestExecutionResult.successful;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import org.apache.maven.surefire.report.ReportEntry;
+import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.report.SimpleReportEntry;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.engine.descriptor.ClassTestDescriptor;
+import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestDescriptor.Type;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.engine.support.descriptor.EngineDescriptor;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+/**
+ * Unit tests for {@link RunListenerAdapter}.
+ *
+ * @since 1.0
+ */
+class RunListenerAdapterTests
+{
+
+    private RunListener listener;
+
+    private RunListenerAdapter adapter;
+
+    @BeforeEach
+    void setUp()
+    {
+        listener = mock( RunListener.class );
+        adapter = new RunListenerAdapter( listener );
+        adapter.testPlanExecutionStarted( TestPlan.from( emptyList() ) );
+    }
+
+    @Test
+    void notifiedWithCorrectNamesWhenMethodExecutionStarted()
+                    throws Exception
+    {
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+
+        TestPlan testPlan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Luke's Plan" ) ) );
+        adapter.testPlanExecutionStarted( testPlan );
+
+        TestIdentifier methodIdentifier =
+                        identifiersAsParentOnTestPlan( testPlan, newClassDescriptor(), newMethodDescriptor() );
+
+        adapter.executionStarted( methodIdentifier );
+        verify( listener ).testStarting( entryCaptor.capture() );
+
+        ReportEntry entry = entryCaptor.getValue();
+        assertEquals( MY_TEST_METHOD_NAME, entry.getName() );
+        assertEquals( MyTestClass.class.getName(), entry.getSourceName() );
+        assertNull( entry.getStackTraceWriter() );
+    }
+
+    @Test
+    void notifiedWithCompatibleNameForMethodWithArguments()
+                    throws Exception
+    {
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+
+        TestPlan testPlan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Luke's Plan" ) ) );
+        adapter.testPlanExecutionStarted( testPlan );
+
+        TestIdentifier methodIdentifier =
+                        identifiersAsParentOnTestPlan(
+                                        testPlan, newClassDescriptor(), newMethodDescriptor( String.class ) );
+
+        adapter.executionStarted( methodIdentifier );
+        verify( listener ).testStarting( entryCaptor.capture() );
+
+        ReportEntry entry = entryCaptor.getValue();
+        assertEquals( MY_TEST_METHOD_NAME + "{String}", entry.getName() );
+        assertEquals( MyTestClass.class.getName(), entry.getSourceName() );
+        assertNull( entry.getStackTraceWriter() );
+    }
+
+    @Test
+    void notifiedEagerlyForTestSetWhenClassExecutionStarted()
+                    throws Exception
+    {
+        EngineDescriptor engine = newEngineDescriptor();
+        TestDescriptor parent = newClassDescriptor();
+        engine.addChild( parent );
+        TestDescriptor child = newMethodDescriptor();
+        parent.addChild( child );
+        TestPlan plan = TestPlan.from( Collections.singletonList( engine ) );
+
+        adapter.testPlanExecutionStarted( plan );
+        adapter.executionStarted( TestIdentifier.from( engine ) );
+        adapter.executionStarted( TestIdentifier.from( parent ) );
+        verify( listener )
+                        .testSetStarting(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        MyTestClass.class.getName() ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionStarted( TestIdentifier.from( child ) );
+        verify( listener )
+                        .testStarting( new SimpleReportEntry( MyTestClass.class.getName(), MY_TEST_METHOD_NAME ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionFinished( TestIdentifier.from( child ), successful() );
+        verify( listener )
+                        .testSucceeded( new SimpleReportEntry( MyTestClass.class.getName(), MY_TEST_METHOD_NAME ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionFinished( TestIdentifier.from( parent ), successful() );
+        verify( listener )
+                        .testSetCompleted(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        MyTestClass.class.getName() ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionFinished( TestIdentifier.from( engine ), successful() );
+        verifyNoMoreInteractions( listener );
+    }
+
+    @Test
+    void notifiedLazilyForTestSetWhenFirstTestWithoutClassDescriptorParentStarted()
+    {
+        EngineDescriptor engine = newEngineDescriptor();
+        TestDescriptor parent =
+                        newTestDescriptor(
+                                        engine.getUniqueId().append( "container", "noClass" ), "parent",
+                                        Type.CONTAINER );
+        engine.addChild( parent );
+        TestDescriptor child1 =
+                        newTestDescriptor( parent.getUniqueId().append( "test", "child1" ), "child1", Type.TEST );
+        parent.addChild( child1 );
+        TestDescriptor child2 =
+                        newTestDescriptor( parent.getUniqueId().append( "test", "child2" ), "child2", Type.TEST );
+        parent.addChild( child2 );
+        TestPlan plan = TestPlan.from( Collections.singletonList( engine ) );
+
+        adapter.testPlanExecutionStarted( plan );
+        adapter.executionStarted( TestIdentifier.from( engine ) );
+        adapter.executionStarted( TestIdentifier.from( parent ) );
+        verifyZeroInteractions( listener );
+
+        adapter.executionStarted( TestIdentifier.from( child1 ) );
+        InOrder inOrder = inOrder( listener );
+        inOrder
+                        .verify( listener )
+                        .testSetStarting( new SimpleReportEntry( JUnitPlatformProvider.class.getName(), "parent" ) );
+        inOrder.verify( listener ).testStarting( new SimpleReportEntry( "parent", "child1" ) );
+        inOrder.verifyNoMoreInteractions();
+
+        adapter.executionFinished( TestIdentifier.from( child1 ), successful() );
+        verify( listener ).testSucceeded( new SimpleReportEntry( "parent", "child1" ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionStarted( TestIdentifier.from( child2 ) );
+        verify( listener ).testStarting( new SimpleReportEntry( "parent", "child2" ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionFinished( TestIdentifier.from( child2 ), successful() );
+        verify( listener ).testSucceeded( new SimpleReportEntry( "parent", "child2" ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionFinished( TestIdentifier.from( parent ), successful() );
+        verify( listener )
+                        .testSetCompleted( new SimpleReportEntry( JUnitPlatformProvider.class.getName(), "parent" ) );
+        verifyNoMoreInteractions( listener );
+
+        adapter.executionFinished( TestIdentifier.from( engine ), successful() );
+        verifyNoMoreInteractions( listener );
+    }
+
+    @Test
+    void notifiedForTestSetForSingleNodeEngine()
+    {
+        EngineDescriptor engine =
+                        new EngineDescriptor( UniqueId.forEngine( "engine" ), "engine" )
+                        {
+                            @Override
+                            public Type getType()
+                            {
+                                return Type.TEST;
+                            }
+                        };
+        TestPlan plan = TestPlan.from( Collections.singletonList( engine ) );
+
+        adapter.testPlanExecutionStarted( plan );
+        adapter.executionStarted( TestIdentifier.from( engine ) );
+        InOrder inOrder = inOrder( listener );
+        inOrder
+                        .verify( listener )
+                        .testSetStarting( new SimpleReportEntry( JUnitPlatformProvider.class.getName(), "engine" ) );
+        inOrder.verify( listener ).testStarting( new SimpleReportEntry( "<unrooted>", "engine" ) );
+        inOrder.verifyNoMoreInteractions();
+
+        adapter.executionFinished( TestIdentifier.from( engine ), successful() );
+        inOrder = inOrder( listener );
+        inOrder.verify( listener ).testSucceeded( new SimpleReportEntry( "<unrooted>", "engine" ) );
+        inOrder
+                        .verify( listener )
+                        .testSetCompleted( new SimpleReportEntry( JUnitPlatformProvider.class.getName(), "engine" ) );
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    void notNotifiedWhenEngineExecutionStarted()
+                    throws Exception
+    {
+        adapter.executionStarted( newEngineIdentifier() );
+        verify( listener, never() ).testStarting( any() );
+    }
+
+    @Test
+    void notifiedWhenMethodExecutionSkipped()
+                    throws Exception
+    {
+        adapter.executionSkipped( newMethodIdentifier(), "test" );
+        verify( listener ).testSkipped( any() );
+    }
+
+    @Test
+    void notifiedWithCorrectNamesWhenClassExecutionSkipped()
+                    throws Exception
+    {
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+        TestPlan testPlan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Luke's Plan" ) ) );
+        adapter.testPlanExecutionStarted( testPlan );
+
+        TestIdentifier classIdentifier =
+                        identifiersAsParentOnTestPlan( testPlan, newEngineDescriptor(), newClassDescriptor() );
+
+        adapter.executionSkipped( classIdentifier, "test" );
+        verify( listener ).testSkipped( entryCaptor.capture() );
+
+        ReportEntry entry = entryCaptor.getValue();
+        assertTrue( MyTestClass.class.getTypeName().contains( entry.getName() ) );
+        assertEquals( MyTestClass.class.getTypeName(), entry.getSourceName() );
+    }
+
+    @Test
+    void notifiedWhenEngineExecutionSkipped()
+                    throws Exception
+    {
+        adapter.executionSkipped( newEngineIdentifier(), "test" );
+        verify( listener ).testSkipped( any() );
+    }
+
+    @Test
+    void notifiedWhenMethodExecutionAborted()
+                    throws Exception
+    {
+        adapter.executionFinished( newMethodIdentifier(), TestExecutionResult.aborted( null ) );
+        verify( listener ).testAssumptionFailure( any() );
+    }
+
+    @Test
+    void notifiedWhenClassExecutionAborted()
+                    throws Exception
+    {
+        adapter.executionFinished( newClassIdentifier(), TestExecutionResult.aborted( null ) );
+        verify( listener ).testAssumptionFailure( any() );
+    }
+
+    @Test
+    void notifiedWhenMethodExecutionFailedWithAnAssertionError()
+                    throws Exception
+    {
+        adapter.executionFinished(
+                        newMethodIdentifier(), TestExecutionResult.failed( new AssertionError() ) );
+        verify( listener ).testFailed( any() );
+    }
+
+    @Test
+    void notifiedWhenMethodExecutionFailedWithANonAssertionError()
+                    throws Exception
+    {
+        adapter.executionFinished(
+                        newMethodIdentifier(), TestExecutionResult.failed( new RuntimeException() ) );
+        verify( listener ).testError( any() );
+    }
+
+    @Test
+    void notifiedWithCorrectNamesWhenClassExecutionFailed()
+                    throws Exception
+    {
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+        TestPlan testPlan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Luke's Plan" ) ) );
+        adapter.testPlanExecutionStarted( testPlan );
+
+        adapter.executionFinished(
+                        identifiersAsParentOnTestPlan( testPlan, newEngineDescriptor(), newClassDescriptor() ),
+                        TestExecutionResult.failed( new AssertionError() ) );
+        verify( listener ).testFailed( entryCaptor.capture() );
+
+        ReportEntry entry = entryCaptor.getValue();
+        assertEquals( MyTestClass.class.getTypeName(), entry.getSourceName() );
+        assertNotNull( entry.getStackTraceWriter() );
+    }
+
+    @Test
+    void notifiedWhenMethodExecutionSucceeded()
+                    throws Exception
+    {
+        adapter.executionFinished( newMethodIdentifier(), successful() );
+        verify( listener ).testSucceeded( any() );
+    }
+
+    @Test
+    void notifiedForTestSetWhenClassExecutionSucceeded()
+                    throws Exception
+    {
+        EngineDescriptor engineDescriptor = newEngineDescriptor();
+        TestDescriptor classDescriptor = newClassDescriptor();
+        engineDescriptor.addChild( classDescriptor );
+        adapter.testPlanExecutionStarted( TestPlan.from( singleton( engineDescriptor ) ) );
+        adapter.executionStarted( TestIdentifier.from( classDescriptor ) );
+
+        adapter.executionFinished( TestIdentifier.from( classDescriptor ), successful() );
+
+        verify( listener )
+                        .testSetCompleted(
+                                        new SimpleReportEntry(
+                                                        JUnitPlatformProvider.class.getName(),
+                                                        MyTestClass.class.getName() ) );
+        verify( listener, never() ).testSucceeded( any() );
+    }
+
+    @Test
+    void notifiedWithParentDisplayNameWhenTestClassUnknown()
+                    throws Exception
+    {
+        // Set up a test plan
+        TestPlan plan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Luke's Plan" ) ) );
+        adapter.testPlanExecutionStarted( plan );
+
+        // Use the test plan to set up child with parent.
+        final String parentDisplay = "I am your father";
+        TestIdentifier child = newSourcelessChildIdentifierWithParent( plan, parentDisplay, null );
+        adapter.executionStarted( child );
+
+        // Check that the adapter has informed Surefire that the test has been invoked,
+        // with the parent name as source (since the test case itself had no source).
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+        verify( listener ).testStarting( entryCaptor.capture() );
+        assertEquals( parentDisplay, entryCaptor.getValue().getSourceName() );
+    }
+
+    @Test
+    void stackTraceWriterPresentWhenParentHasSource()
+                    throws Exception
+    {
+        TestPlan plan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Some Plan" ) ) );
+        adapter.testPlanExecutionStarted( plan );
+
+        TestIdentifier child =
+                        newSourcelessChildIdentifierWithParent( plan, "Parent", ClassSource.from( MyTestClass.class ) );
+        adapter.executionFinished( child, TestExecutionResult.failed( new RuntimeException() ) );
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+        verify( listener ).testError( entryCaptor.capture() );
+        assertNotNull( entryCaptor.getValue().getStackTraceWriter() );
+    }
+
+    @Test
+    void stackTraceWriterDefaultsToTestClass()
+                    throws Exception
+    {
+        TestPlan plan =
+                        TestPlan.from( Collections.singletonList( new EngineDescriptor( newId(), "Some Plan" ) ) );
+        adapter.testPlanExecutionStarted( plan );
+
+        TestIdentifier child = newSourcelessChildIdentifierWithParent( plan, "Parent", null );
+        adapter.executionFinished( child, TestExecutionResult.failed( new RuntimeException( "message" ) ) );
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+        verify( listener ).testError( entryCaptor.capture() );
+        assertNotNull( entryCaptor.getValue().getStackTraceWriter() );
+        assertNotNull( entryCaptor.getValue().getStackTraceWriter().smartTrimmedStackTrace() );
+        assertNotNull( entryCaptor.getValue().getStackTraceWriter().writeTraceToString() );
+        assertNotNull( entryCaptor.getValue().getStackTraceWriter().writeTrimmedTraceToString() );
+    }
+
+    @Test
+    void stackTraceWriterPresentEvenWithoutException()
+                    throws Exception
+    {
+        adapter.executionFinished( newMethodIdentifier(), TestExecutionResult.failed( null ) );
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+        verify( listener ).testError( entryCaptor.capture() );
+        assertNotNull( entryCaptor.getValue().getStackTraceWriter() );
+    }
+
+    @Test
+    void displayNamesIgnoredInReport()
+                    throws NoSuchMethodException
+    {
+        TestMethodTestDescriptor descriptor =
+                        new TestMethodTestDescriptor(
+                                        newId(), MyTestClass.class,
+                                        MyTestClass.class.getDeclaredMethod( "myNamedTestMethod" ) );
+
+        TestIdentifier factoryIdentifier = TestIdentifier.from( descriptor );
+        ArgumentCaptor<ReportEntry> entryCaptor = ArgumentCaptor.forClass( ReportEntry.class );
+
+        adapter.executionSkipped( factoryIdentifier, "" );
+        verify( listener ).testSkipped( entryCaptor.capture() );
+
+        ReportEntry value = entryCaptor.getValue();
+
+        assertEquals( "myNamedTestMethod", value.getName() );
+    }
+
+    private static TestIdentifier newMethodIdentifier()
+                    throws Exception
+    {
+        return TestIdentifier.from( newMethodDescriptor() );
+    }
+
+    private static TestDescriptor newMethodDescriptor( Class<?>... parameterTypes )
+                    throws Exception
+    {
+        return new TestMethodTestDescriptor(
+                        UniqueId.forEngine( "method" ),
+                        MyTestClass.class,
+                        MyTestClass.class.getDeclaredMethod( MY_TEST_METHOD_NAME, parameterTypes ) );
+    }
+
+    private static TestIdentifier newClassIdentifier()
+    {
+        return TestIdentifier.from( newClassDescriptor() );
+    }
+
+    private static TestDescriptor newClassDescriptor()
+    {
+        return new ClassTestDescriptor(
+                        UniqueId.root( "class", MyTestClass.class.getName() ), MyTestClass.class );
+    }
+
+    private static TestIdentifier newSourcelessChildIdentifierWithParent(
+                    TestPlan testPlan, String parentDisplay, TestSource parentTestSource )
+    {
+        // A parent test identifier with a name.
+        TestDescriptor parent = mock( TestDescriptor.class );
+        when( parent.getUniqueId() ).thenReturn( newId() );
+        when( parent.getDisplayName() ).thenReturn( parentDisplay );
+        when( parent.getLegacyReportingName() ).thenReturn( parentDisplay );
+        when( parent.getSource() ).thenReturn( Optional.ofNullable( parentTestSource ) );
+        when( parent.getType() ).thenReturn( Type.CONTAINER );
+        TestIdentifier parentId = TestIdentifier.from( parent );
+
+        // The (child) test case that is to be executed as part of a test plan.
+        TestDescriptor child = mock( TestDescriptor.class );
+        when( child.getUniqueId() ).thenReturn( newId() );
+        when( child.getType() ).thenReturn( Type.TEST );
+        when( child.getLegacyReportingName() ).thenReturn( "child" );
+
+        // Ensure the child source is null yet that there is a parent -- the special case to be tested.
+        when( child.getSource() ).thenReturn( Optional.empty() );
+        when( child.getParent() ).thenReturn( Optional.of( parent ) );
+        TestIdentifier childId = TestIdentifier.from( child );
+
+        testPlan.add( childId );
+        testPlan.add( parentId );
+
+        return childId;
+    }
+
+    private static TestIdentifier newEngineIdentifier()
+    {
+        TestDescriptor testDescriptor = newEngineDescriptor();
+        return TestIdentifier.from( testDescriptor );
+    }
+
+    private static EngineDescriptor newEngineDescriptor()
+    {
+        return new EngineDescriptor( UniqueId.forEngine( "engine" ), "engine" );
+    }
+
+    private TestDescriptor newTestDescriptor( UniqueId uniqueId, String displayName, Type type )
+    {
+        return new AbstractTestDescriptor( uniqueId, displayName )
+        {
+            @Override
+            public Type getType()
+            {
+                return type;
+            }
+        };
+    }
+
+    private static TestIdentifier identifiersAsParentOnTestPlan(
+                    TestPlan plan, TestDescriptor parent, TestDescriptor child )
+    {
+        child.setParent( parent );
+
+        TestIdentifier parentIdentifier = TestIdentifier.from( parent );
+        TestIdentifier childIdentifier = TestIdentifier.from( child );
+
+        plan.add( parentIdentifier );
+        plan.add( childIdentifier );
+
+        return childIdentifier;
+    }
+
+    private static UniqueId newId()
+    {
+        return UniqueId.forEngine( "engine" );
+    }
+
+    private static final String MY_TEST_METHOD_NAME = "myTestMethod";
+
+    private static class MyTestClass
+    {
+        @Test
+        void myTestMethod()
+        {
+        }
+
+        @Test
+        void myTestMethod( String foo )
+        {
+        }
+
+        @DisplayName( "name" )
+        @Test
+        void myNamedTestMethod()
+        {
+        }
+    }
+}
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/TestMethodFilterTests.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/TestMethodFilterTests.java
new file mode 100644
index 0000000..29ca326
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/TestMethodFilterTests.java
@@ -0,0 +1,105 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static org.apache.maven.surefire.testset.TestListResolver.toClassFileName;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+
+import org.apache.maven.surefire.testset.TestListResolver;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.engine.descriptor.ClassTestDescriptor;
+import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.UniqueId;
+
+/**
+ * Unit tests for {@link TestMethodFilter}.
+ *
+ * @since 1.0.3
+ */
+class TestMethodFilterTests
+{
+
+    private final TestListResolver resolver = mock( TestListResolver.class );
+
+    private final TestMethodFilter filter = new TestMethodFilter( this.resolver );
+
+    @Test
+    void includesBasedOnTestListResolver()
+                    throws Exception
+    {
+        when( resolver.shouldRun( toClassFileName( TestClass.class ), "testMethod" ) ).thenReturn( true );
+
+        FilterResult result = filter.apply( newTestMethodDescriptor() );
+
+        assertTrue( result.included() );
+        assertFalse( result.excluded() );
+    }
+
+    @Test
+    void excludesBasedOnTestListResolver()
+                    throws Exception
+    {
+        when( resolver.shouldRun( toClassFileName( TestClass.class ), "testMethod" ) ).thenReturn( false );
+
+        FilterResult result = filter.apply( newTestMethodDescriptor() );
+
+        assertFalse( result.included() );
+        assertTrue( result.excluded() );
+    }
+
+    @Test
+    void includesTestDescriptorWithClassSource()
+                    throws Exception
+    {
+        FilterResult result = filter.apply( newClassTestDescriptor() );
+
+        assertTrue( result.included() );
+        assertFalse( result.excluded() );
+    }
+
+    private static TestMethodTestDescriptor newTestMethodDescriptor()
+                    throws Exception
+    {
+        UniqueId uniqueId = UniqueId.forEngine( "method" );
+        Class<TestClass> testClass = TestClass.class;
+        Method testMethod = testClass.getMethod( "testMethod" );
+        return new TestMethodTestDescriptor( uniqueId, testClass, testMethod );
+    }
+
+    private static ClassTestDescriptor newClassTestDescriptor()
+                    throws Exception
+    {
+        UniqueId uniqueId = UniqueId.forEngine( "class" );
+        return new ClassTestDescriptor( uniqueId, TestClass.class );
+    }
+
+    public static class TestClass
+    {
+        public void testMethod()
+        {
+        }
+    }
+}
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/TestPlanScannerFilterTests.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/TestPlanScannerFilterTests.java
new file mode 100644
index 0000000..8ecedc7
--- /dev/null
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/TestPlanScannerFilterTests.java
@@ -0,0 +1,188 @@
+package org.apache.maven.surefire.junitplatform;
+
+/*
+ * 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 static java.util.Collections.emptyList;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.platform.engine.Filter;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+/**
+ * Unit tests for {@link TestPlanScannerFilter}.
+ *
+ * @since 1.0
+ */
+class TestPlanScannerFilterTests
+{
+
+    @Test
+    void emptyClassIsNotAccepted()
+    {
+        assertFalse( newFilter().accept( EmptyClass.class ), "does not accept empty class" );
+    }
+
+    @Test
+    void classWithNoTestMethodsIsNotAccepted()
+    {
+        assertFalse(
+                        newFilter().accept( ClassWithMethods.class ), "does not accept class with no @Test methods" );
+    }
+
+    @Test
+    void classWithTestMethodsIsAccepted()
+    {
+        assertTrue( newFilter().accept( ClassWithTestMethods.class ) );
+    }
+
+    @Test
+    void classWithNestedTestClassIsAccepted()
+    {
+        assertTrue( newFilter().accept( ClassWithNestedTestClass.class ) );
+    }
+
+    @Test
+    void classWithDeeplyNestedTestClassIsAccepted()
+    {
+        assertTrue( newFilter().accept( ClassWithDeeplyNestedTestClass.class ) );
+    }
+
+    @Test
+    void classWithTestFactoryIsAccepted()
+    {
+        assertTrue( newFilter().accept( ClassWithTestFactory.class ) );
+    }
+
+    @Test
+    void classWithNestedTestFactoryIsAccepted()
+    {
+        assertTrue( newFilter().accept( ClassWithNestedTestFactory.class ) );
+    }
+
+    private TestPlanScannerFilter newFilter()
+    {
+        return new TestPlanScannerFilter( LauncherFactory.create(), new Filter<?>[0] );
+    }
+
+    static class EmptyClass
+    {
+    }
+
+    static class ClassWithMethods
+    {
+
+        void method1()
+        {
+        }
+
+        void method2()
+        {
+        }
+    }
+
+    static class ClassWithTestMethods
+    {
+
+        @Test
+        void test1()
+        {
+        }
+
+        @Test
+        public void test2()
+        {
+        }
+    }
+
+    static class ClassWithNestedTestClass
+    {
+
+        void method()
+        {
+        }
+
+        @Nested
+        class TestClass
+        {
+
+            @Test
+            void test1()
+            {
+            }
+        }
+    }
+
+    static class ClassWithDeeplyNestedTestClass
+    {
+
+        @Nested
+        class Level1
+        {
+
+            @Nested
+            class Level2
+            {
+
+                @Nested
+                class TestClass
+                {
+
+                    @Test
+                    void test1()
+                    {
+                    }
+                }
+            }
+        }
+    }
+
+    static class ClassWithTestFactory
+    {
+
+        @TestFactory
+        Stream<DynamicTest> tests()
+        {
+            return Stream.empty();
+        }
+    }
+
+    static class ClassWithNestedTestFactory
+    {
+
+        @Nested
+        class TestClass
+        {
+
+            @TestFactory
+            List<DynamicTest> tests()
+            {
+                return emptyList();
+            }
+        }
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
tibordigana@apache.org.