Skip to content

Commit 45102fa

Browse files
authored
[SUREFIRE-3446] Fix direct selection of JUnit Jupiter @nested classes (#3447)
* Add failing IT for #3446 The direct selection of a JUnit Jupiter @nested class runs no tests on current master, although the same selection runs with Surefire 3.5.4. Refs #3446 * [SUREFIRE-3446] Fix direct selection of nested JUnit 5 tests Allow the actual enclosing class hierarchy through JUnit Platform class-name filters so a directly selected @nested class can be discovered without broadening test execution. Derive that hierarchy from Class metadata rather than binary-name parsing, and cover ordinary, regex, multi-level lifecycle, sibling, and legal dollar-sign cases. Fixes #3446
1 parent b2e1f70 commit 45102fa

5 files changed

Lines changed: 260 additions & 4 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.surefire.its.jiras;
20+
21+
import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
22+
import org.junit.jupiter.api.Test;
23+
24+
/**
25+
* Integration Test for #3446.
26+
*/
27+
public class Surefire3446IT extends SurefireJUnit4IntegrationTestCase {
28+
@Test
29+
void shouldRunSelectedNestedClass() {
30+
unpack("surefire-3446-nested-selection")
31+
.setTestToRun("issue3446.NestedTest$Intermediate$Selected")
32+
.executeTest()
33+
.verifyErrorFree(1);
34+
}
35+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one
4+
~ or more contributor license agreements. See the NOTICE file
5+
~ distributed with this work for additional information
6+
~ regarding copyright ownership. The ASF licenses this file
7+
~ to you under the Apache License, Version 2.0 (the
8+
~ "License"); you may not use this file except in compliance
9+
~ with the License. You may obtain a copy of the License at
10+
~
11+
~ http://www.apache.org/licenses/LICENSE-2.0
12+
~
13+
~ Unless required by applicable law or agreed to in writing,
14+
~ software distributed under the License is distributed on an
15+
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
~ KIND, either express or implied. See the License for the
17+
~ specific language governing permissions and limitations
18+
~ under the License.
19+
-->
20+
21+
<project xmlns="http://maven.apache.org/POM/4.0.0"
22+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
23+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
24+
<modelVersion>4.0.0</modelVersion>
25+
26+
<groupId>org.apache.maven.plugins.surefire</groupId>
27+
<artifactId>surefire-3446-nested-selection</artifactId>
28+
<version>1.0</version>
29+
<name>Test for direct selection of a JUnit Jupiter nested class</name>
30+
31+
<properties>
32+
<maven.compiler.source>1.8</maven.compiler.source>
33+
<maven.compiler.target>1.8</maven.compiler.target>
34+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
35+
<junit5.version>5.14.4</junit5.version>
36+
</properties>
37+
38+
<dependencies>
39+
<dependency>
40+
<groupId>org.junit.jupiter</groupId>
41+
<artifactId>junit-jupiter-engine</artifactId>
42+
<version>${junit5.version}</version>
43+
<scope>test</scope>
44+
</dependency>
45+
</dependencies>
46+
47+
<build>
48+
<plugins>
49+
<plugin>
50+
<artifactId>maven-compiler-plugin</artifactId>
51+
<version>3.8.0</version>
52+
</plugin>
53+
<plugin>
54+
<groupId>org.apache.maven.plugins</groupId>
55+
<artifactId>maven-surefire-plugin</artifactId>
56+
<version>${surefire.version}</version>
57+
</plugin>
58+
</plugins>
59+
</build>
60+
</project>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package issue3446;
20+
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Nested;
23+
import org.junit.jupiter.api.Test;
24+
25+
import static org.junit.jupiter.api.Assertions.assertTrue;
26+
import static org.junit.jupiter.api.Assertions.fail;
27+
28+
class NestedTest {
29+
private boolean outerSetUp;
30+
31+
@BeforeEach
32+
void setUpOuter() {
33+
outerSetUp = true;
34+
}
35+
36+
@Test
37+
void outerTestMustNotRun() {
38+
fail("The outer test must not run");
39+
}
40+
41+
@Nested
42+
class Intermediate {
43+
private boolean intermediateSetUp;
44+
45+
@BeforeEach
46+
void setUpIntermediate() {
47+
assertTrue(outerSetUp);
48+
intermediateSetUp = true;
49+
}
50+
51+
@Test
52+
void intermediateTestMustNotRun() {
53+
fail("The intermediate test must not run");
54+
}
55+
56+
@Nested
57+
class Selected {
58+
@Test
59+
void selectedTest() {
60+
assertTrue(outerSetUp);
61+
assertTrue(intermediateSetUp);
62+
}
63+
}
64+
65+
@Nested
66+
class Sibling {
67+
@Test
68+
void siblingTestMustNotRun() {
69+
fail("A sibling nested test must not run");
70+
}
71+
}
72+
}
73+
}

surefire-providers/surefire-junit-platform/src/main/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProvider.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.Map;
3232
import java.util.Optional;
3333
import java.util.Properties;
34+
import java.util.Set;
3435
import java.util.StringTokenizer;
3536
import java.util.concurrent.atomic.AtomicBoolean;
3637
import java.util.concurrent.atomic.AtomicInteger;
@@ -441,14 +442,16 @@ private Filter<?>[] newFilters() {
441442
// includeClassNamePatterns support only regex patterns
442443
Optional<String> includesList =
443444
Optional.ofNullable(parameters.getProviderProperties().get(ProviderParameterNames.INCLUDES_SCAN_LIST));
445+
Set<String> enclosingClassNames = includesList.isPresent() ? getEnclosingClassNames() : Collections.emptySet();
444446
if (includesList.isPresent()) {
445447
String[] includesRegex = Stream.of(includesList.get().split(","))
446448
.filter(s -> s.startsWith("%regex["))
447449
.map(s -> StringUtils.replace(s, "%regex[", ""))
448450
.map(s -> s.substring(0, s.length() - 1))
449451
.toArray(String[]::new);
450452
if (includesRegex.length > 0) {
451-
filters.add(ClassNameFilter.includeClassNamePatterns(includesRegex));
453+
filters.add(includeEnclosingClasses(
454+
ClassNameFilter.includeClassNamePatterns(includesRegex), enclosingClassNames));
452455
}
453456
}
454457

@@ -475,16 +478,16 @@ private Filter<?>[] newFilters() {
475478
.collect(toList());
476479
if (!includes.isEmpty()) {
477480
// use of CompositeFilter?
478-
ClassNameFilter classNameFilter = clasName -> {
481+
ClassNameFilter classNameFilter = className -> {
479482
FilterResult result = includes.stream()
480483
.map(pattern -> FilterResult.includedIf(
481-
match(pattern, clasName) || matchClassName(clasName, pattern)))
484+
match(pattern, className) || matchClassName(className, pattern)))
482485
.filter(FilterResult::included)
483486
.findAny()
484487
.orElse(FilterResult.excluded("Not included by any pattern: " + includes));
485488
return result;
486489
};
487-
filters.add(classNameFilter);
490+
filters.add(includeEnclosingClasses(classNameFilter, enclosingClassNames));
488491
}
489492
}
490493

@@ -553,6 +556,33 @@ private Filter<?>[] newFilters() {
553556
return filters.toArray(new Filter<?>[0]);
554557
}
555558

559+
private Set<String> getEnclosingClassNames() {
560+
Set<String> enclosingClassNames = new LinkedHashSet<>();
561+
ScanResult scanResult = parameters.getScanResult();
562+
for (int i = 0; i < scanResult.size(); i++) {
563+
String className = scanResult.getClassName(i);
564+
Class<?> testClass;
565+
try {
566+
testClass = parameters.getTestClassLoader().loadClass(className);
567+
} catch (ClassNotFoundException e) {
568+
throw new RuntimeException("Unable to create test class '" + className + "'", e);
569+
}
570+
for (Class<?> enclosingClass = testClass.getEnclosingClass();
571+
enclosingClass != null;
572+
enclosingClass = enclosingClass.getEnclosingClass()) {
573+
enclosingClassNames.add(enclosingClass.getName());
574+
}
575+
}
576+
return enclosingClassNames;
577+
}
578+
579+
private static ClassNameFilter includeEnclosingClasses(
580+
ClassNameFilter classNameFilter, Set<String> enclosingClassNames) {
581+
return className -> enclosingClassNames.contains(className)
582+
? FilterResult.included("Enclosing class of an included test class")
583+
: classNameFilter.apply(className);
584+
}
585+
556586
Filter<?>[] getFilters() {
557587
return filters;
558588
}

surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Map;
3030
import java.util.Set;
3131
import java.util.function.UnaryOperator;
32+
import java.util.regex.Pattern;
3233

3334
import org.apache.maven.surefire.api.provider.ProviderParameters;
3435
import org.apache.maven.surefire.api.report.ReportEntry;
@@ -55,6 +56,7 @@
5556
import org.junit.platform.engine.TestDescriptor;
5657
import org.junit.platform.engine.TestExecutionResult;
5758
import org.junit.platform.engine.UniqueId;
59+
import org.junit.platform.engine.discovery.ClassNameFilter;
5860
import org.junit.platform.engine.discovery.ClassSelector;
5961
import org.junit.platform.engine.discovery.UniqueIdSelector;
6062
import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
@@ -81,6 +83,7 @@
8183
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.EXCLUDEDGROUPS_PROP;
8284
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.EXCLUDE_JUNIT5_ENGINES_PROP;
8385
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.GROUPS_PROP;
86+
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.INCLUDES_SCAN_LIST;
8487
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.INCLUDE_JUNIT5_ENGINES_PROP;
8588
import static org.apache.maven.surefire.api.report.RunMode.NORMAL_RUN;
8689
import static org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.CONFIGURATION_PARAMETERS;
@@ -560,6 +563,51 @@ public void runNestingTest() throws Exception {
560563
assertNull(reportEntries.get(1).getNameText());
561564
}
562565

566+
@Test
567+
public void runsDirectlySelectedNestedClass() throws Exception {
568+
assertDirectNestedClassSelection(NestingTest.Level1NestedTest.Level2NestedTest.class.getName());
569+
}
570+
571+
@Test
572+
public void runsDirectlySelectedNestedClassWithRegexInclude() throws Exception {
573+
String className = NestingTest.Level1NestedTest.Level2NestedTest.class.getName();
574+
assertDirectNestedClassSelection("%regex[" + Pattern.quote(className) + "]");
575+
}
576+
577+
@Test
578+
public void doesNotTreatDollarAsNestedClassSeparator() {
579+
TestListResolver testListResolver = new TestListResolver(LegalDollarClass$Test.class.getName());
580+
ProviderParameters parameters = providerParametersMock(testListResolver, LegalDollarClass$Test.class);
581+
when(parameters.getProviderProperties())
582+
.thenReturn(singletonMap(INCLUDES_SCAN_LIST, LegalDollarClass$Test.class.getName()));
583+
584+
JUnitPlatformProvider provider = new JUnitPlatformProvider(parameters);
585+
586+
assertThat(provider.getFilters()).hasSize(1);
587+
ClassNameFilter includeFilter = (ClassNameFilter) provider.getFilters()[0];
588+
assertTrue(includeFilter.apply(LegalDollarClass$Test.class.getName()).included());
589+
assertFalse(includeFilter.apply(LegalDollarClass.class.getName()).included());
590+
}
591+
592+
private static void assertDirectNestedClassSelection(String includeScanPattern) throws Exception {
593+
Class<?> selectedClass = NestingTest.Level1NestedTest.Level2NestedTest.class;
594+
TestListResolver testListResolver = new TestListResolver(selectedClass.getName());
595+
ProviderParameters parameters = providerParametersMock(testListResolver, selectedClass);
596+
when(parameters.getProviderProperties()).thenReturn(singletonMap(INCLUDES_SCAN_LIST, includeScanPattern));
597+
598+
TestPlanSummaryListener executionListener = new TestPlanSummaryListener();
599+
JUnitPlatformProvider provider =
600+
new JUnitPlatformProvider(parameters, createLauncherSessionWithListeners(executionListener));
601+
602+
invokeProvider(provider, null);
603+
604+
assertThat(executionListener.summaries).hasSize(1);
605+
TestExecutionSummary summary = executionListener.summaries.get(0);
606+
assertEquals(1, summary.getTestsFoundCount());
607+
assertEquals(1, summary.getTestsSucceededCount());
608+
assertEquals(0, summary.getTestsFailedCount());
609+
}
610+
563611
@Test
564612
public void detectSkippedParameterized() throws Exception {
565613
ProviderParameters parameters = providerParametersMock();
@@ -1194,6 +1242,10 @@ private static ProviderParameters providerParametersMock(
11941242

11951243
ScanResult scanResult = mock(ScanResult.class);
11961244
when(scanResult.applyFilter(any(), any())).thenReturn(testsToRun);
1245+
when(scanResult.size()).thenReturn(testClasses.length);
1246+
for (int i = 0; i < testClasses.length; i++) {
1247+
when(scanResult.getClassName(i)).thenReturn(testClasses[i].getName());
1248+
}
11971249

11981250
RunOrderCalculator runOrderCalculator = mock(RunOrderCalculator.class);
11991251
when(runOrderCalculator.orderTestClasses(any())).thenReturn(testsToRun);
@@ -1209,6 +1261,7 @@ private static ProviderParameters providerParametersMock(
12091261
when(providerParameters.getRunOrderCalculator()).thenReturn(runOrderCalculator);
12101262
when(providerParameters.getReporterFactory()).thenReturn(reporterFactory);
12111263
when(providerParameters.getTestRequest()).thenReturn(testRequest);
1264+
when(providerParameters.getTestClassLoader()).thenReturn(JUnitPlatformProviderTest.class.getClassLoader());
12121265

12131266
return providerParameters;
12141267
}
@@ -1592,3 +1645,8 @@ private static <T> T getInternalState(Object target, String fieldName) throws Ex
15921645
return (T) field.get(target);
15931646
}
15941647
}
1648+
1649+
class LegalDollarClass {}
1650+
1651+
@SuppressWarnings("checkstyle:typename")
1652+
class LegalDollarClass$Test {}

0 commit comments

Comments
 (0)