Skip to content

Commit d2c8d60

Browse files
authored
Introduce discovery listeners and abort on failures by default
The `Launcher` now propagates errors during test discovery by default instead of only logging and thereby potentially hiding them. In order to restore the old, lenient behavior, the `junit.platform.discovery.listener.default` configuration parameter can be set to `logging`. To support the above feature consistently, a new `EngineDiscoveryListener` interface was introduced. `TestEngine` implementations should now notify the listener that can be accessed via the `EngineDiscoveryRequest.getDiscoveryListener()` method about each processed `DiscoverySelector`. Test engines that use `EngineDiscoveryRequestResolver` do not have to make any changes. In addition, clients of the `Launcher` such as build tools and IDEs can add `LauncherDiscoveryListeners` to `LauncherDiscoveryRequests` which will be notified about the above events and the start and finish of each engine's test discovery. Resolves #2052.
1 parent 614f6d5 commit d2c8d60

File tree

35 files changed

+1347
-167
lines changed

35 files changed

+1347
-167
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ endif::[]
2727
// Platform Launcher API
2828
:junit-platform-launcher: {javadoc-root}/org/junit/platform/launcher/package-summary.html[junit-platform-launcher]
2929
:Launcher: {javadoc-root}/org/junit/platform/launcher/Launcher.html[Launcher]
30+
:LauncherDiscoveryListener: {javadoc-root}/org/junit/platform/launcher/LauncherDiscoveryListener.html[LauncherDiscoveryListener]
31+
:LauncherDiscoveryRequestBuilder: {javadoc-root}/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.html[LauncherDiscoveryRequestBuilder]
3032
:LoggingListener: {javadoc-root}/org/junit/platform/launcher/listeners/LoggingListener.html[LoggingListener]
3133
:SummaryGeneratingListener: {javadoc-root}/org/junit/platform/launcher/listeners/SummaryGeneratingListener.html[SummaryGeneratingListener]
3234
:TestExecutionListener: {javadoc-root}/org/junit/platform/launcher/TestExecutionListener.html[TestExecutionListener]

documentation/src/docs/asciidoc/release-notes/release-notes-5.6.0-M1.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ on GitHub.
3535

3636
==== Deprecations and Breaking Changes
3737

38+
* The `Launcher` now propagates errors during test discovery by default instead of only
39+
logging and thereby potentially hiding them. In order to restore the old, lenient
40+
behavior, you can set the `junit.platform.discovery.listener.default` configuration
41+
parameter to `logging`.
42+
* To support the above feature consistently, a new `EngineDiscoveryListener` interface was
43+
introduced. `TestEngine` implementations should now notify the listener that can be
44+
accessed via the `EngineDiscoveryRequest.getDiscoveryListener()` method about each
45+
processed `DiscoverySelector`. Test engines that use `EngineDiscoveryRequestResolver` do
46+
not have to make any changes.
3847
* In the `EngineTestKit` API, the `all()`, `containers()`, and `tests()` methods in
3948
`EngineExecutionResults` have been deprecated in favor of the new `allEvents()`,
4049
`containerEvents()`, and `testEvents()` methods, respectively. The deprecated methods

documentation/src/docs/asciidoc/user-guide/launcher-api.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ traverse the tree, retrieve details about a node, and get a link to the original
4545
(like class, method, or file position). Every node in the test plan has a _unique ID_
4646
that can be used to invoke a particular test or group of tests.
4747

48+
Clients can register one or more `{LauncherDiscoveryListener}` implementations to get
49+
insights into events that occur during test discovery via the
50+
`{LauncherDiscoveryRequestBuilder}`. The builder registers a default listener that can be
51+
changed via the `junit.platform.discovery.listener.default` configuration parameter. If
52+
the parameter is not set, test discovery will be aborted after the first failure is
53+
encountered.
54+
4855
[[launcher-api-execution]]
4956
==== Executing Tests
5057

junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java

Lines changed: 70 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod;
2626
import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTopLevelClass;
2727
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
28+
import static org.junit.platform.engine.SelectorResolutionResult.Status.FAILED;
29+
import static org.junit.platform.engine.SelectorResolutionResult.Status.RESOLVED;
30+
import static org.junit.platform.engine.SelectorResolutionResult.Status.UNRESOLVED;
2831
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
2932
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
3033
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathRoots;
@@ -33,8 +36,11 @@
3336
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
3437
import static org.junit.platform.engine.discovery.PackageNameFilter.excludePackageNames;
3538
import static org.junit.platform.engine.discovery.PackageNameFilter.includePackageNames;
36-
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
39+
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.DEFAULT_DISCOVERY_LISTENER_CONFIGURATION_PROPERTY_NAME;
40+
import static org.mockito.ArgumentMatchers.any;
41+
import static org.mockito.ArgumentMatchers.eq;
3742
import static org.mockito.Mockito.mock;
43+
import static org.mockito.Mockito.verify;
3844
import static org.mockito.Mockito.when;
3945

4046
import java.lang.reflect.Method;
@@ -44,8 +50,7 @@
4450
import java.nio.file.Paths;
4551
import java.util.ArrayList;
4652
import java.util.List;
47-
import java.util.logging.Level;
48-
import java.util.logging.LogRecord;
53+
import java.util.function.Predicate;
4954
import java.util.stream.Stream;
5055

5156
import org.junit.jupiter.api.BeforeEach;
@@ -55,7 +60,6 @@
5560
import org.junit.jupiter.api.Test;
5661
import org.junit.jupiter.api.TestFactory;
5762
import org.junit.jupiter.api.TestTemplate;
58-
import org.junit.jupiter.api.fixtures.TrackLogRecords;
5963
import org.junit.jupiter.api.parallel.ExecutionMode;
6064
import org.junit.jupiter.engine.config.JupiterConfiguration;
6165
import org.junit.jupiter.engine.descriptor.DynamicDescendantFilter;
@@ -68,24 +72,27 @@
6872
import org.junit.jupiter.engine.descriptor.subpackage.ClassWithStaticInnerTestCases;
6973
import org.junit.platform.commons.JUnitException;
7074
import org.junit.platform.commons.PreconditionViolationException;
71-
import org.junit.platform.commons.logging.LogRecordListener;
7275
import org.junit.platform.commons.util.ReflectionUtils;
76+
import org.junit.platform.engine.DiscoverySelector;
77+
import org.junit.platform.engine.SelectorResolutionResult;
7378
import org.junit.platform.engine.TestDescriptor;
7479
import org.junit.platform.engine.UniqueId;
7580
import org.junit.platform.engine.discovery.ClassSelector;
7681
import org.junit.platform.engine.discovery.ClasspathRootSelector;
7782
import org.junit.platform.engine.discovery.MethodSelector;
7883
import org.junit.platform.engine.discovery.PackageSelector;
7984
import org.junit.platform.engine.discovery.UniqueIdSelector;
80-
import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver;
85+
import org.junit.platform.launcher.LauncherDiscoveryListener;
8186
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
87+
import org.mockito.ArgumentCaptor;
8288

8389
/**
8490
* @since 5.0
8591
*/
8692
class DiscoverySelectorResolverTests {
8793

8894
private final JupiterConfiguration configuration = mock(JupiterConfiguration.class);
95+
private final LauncherDiscoveryListener discoveryListener = mock(LauncherDiscoveryListener.class);
8996
private final JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(engineId(), configuration);
9097

9198
@BeforeEach
@@ -102,14 +109,11 @@ void nonTestClassResolution() {
102109
}
103110

104111
@Test
105-
@TrackLogRecords
106-
void abstractClassResolution(LogRecordListener listener) {
112+
void abstractClassResolution() {
107113
resolve(request().selectors(selectClass(AbstractTestClass.class)));
108114

109115
assertTrue(engineDescriptor.getDescendants().isEmpty());
110-
assertThat(firstDebugLogRecord(listener).getMessage())//
111-
.isEqualTo(
112-
"ClassSelector [className = '" + AbstractTestClass.class.getName() + "'] could not be resolved.");
116+
assertUnresolved(selectClass(AbstractTestClass.class));
113117
}
114118

115119
@Test
@@ -123,15 +127,15 @@ void singleClassResolution() {
123127
}
124128

125129
@Test
126-
@TrackLogRecords
127-
void classResolutionForNonexistentClass(LogRecordListener listener) {
130+
void classResolutionForNonexistentClass() {
128131
ClassSelector selector = selectClass("org.example.DoesNotExist");
129132

130133
resolve(request().selectors(selector));
131134

132135
assertTrue(engineDescriptor.getDescendants().isEmpty());
133-
assertThat(firstDebugLogRecord(listener).getMessage())//
134-
.isEqualTo("ClassSelector [className = 'org.example.DoesNotExist'] could not be resolved.");
136+
var result = verifySelectorProcessed(selector);
137+
assertThat(result.getStatus()).isEqualTo(FAILED);
138+
assertThat(result.getThrowable().get()).hasMessageContaining("Could not load class with name");
135139
}
136140

137141
@Test
@@ -216,35 +220,31 @@ void resolvingSelectorOfNonTestMethodResolvesNothing() throws NoSuchMethodExcept
216220
}
217221

218222
@Test
219-
@TrackLogRecords
220-
void methodResolutionForNonexistentClass(LogRecordListener listener) {
223+
void methodResolutionForNonexistentClass() {
221224
String className = "org.example.DoesNotExist";
222225
String methodName = "bogus";
223226
MethodSelector selector = selectMethod(className, methodName, "");
224227

225228
resolve(request().selectors(selector));
226229

227230
assertTrue(engineDescriptor.getDescendants().isEmpty());
228-
LogRecord logRecord = firstDebugLogRecord(listener);
229-
assertThat(logRecord.getMessage())//
230-
.startsWith("MethodSelector").endsWith("could not be resolved.")//
231-
.contains(className, methodName);
232-
assertThat(logRecord.getThrown())//
231+
var result = verifySelectorProcessed(selector);
232+
assertThat(result.getStatus()).isEqualTo(FAILED);
233+
assertThat(result.getThrowable().get())//
233234
.isInstanceOf(PreconditionViolationException.class)//
234235
.hasMessageStartingWith("Could not load class with name: " + className);
235236
}
236237

237238
@Test
238-
@TrackLogRecords
239-
void methodResolutionForNonexistentMethod(LogRecordListener listener) {
239+
void methodResolutionForNonexistentMethod() {
240240
MethodSelector selector = selectMethod(MyTestClass.class, "bogus", "");
241241

242242
resolve(request().selectors(selector));
243243

244244
assertTrue(engineDescriptor.getDescendants().isEmpty());
245-
assertThat(firstDebugLogRecord(listener).getMessage())//
246-
.startsWith("MethodSelector").endsWith("could not be resolved.")//
247-
.contains(MyTestClass.class.getName(), "bogus");
245+
var result = verifySelectorProcessed(selector);
246+
assertThat(result.getStatus()).isEqualTo(FAILED);
247+
assertThat(result.getThrowable().get()).hasMessageContaining("Could not find method");
248248
}
249249

250250
@Test
@@ -285,16 +285,14 @@ void methodOfInnerClassByUniqueId() {
285285
}
286286

287287
@Test
288-
@TrackLogRecords
289-
void resolvingUniqueIdWithUnknownSegmentTypeResolvesNothing(LogRecordListener listener) {
288+
void resolvingUniqueIdWithUnknownSegmentTypeResolvesNothing() {
290289
UniqueId uniqueId = engineId().append("bogus", "enigma");
291290
UniqueIdSelector selector = selectUniqueId(uniqueId);
292291

293292
resolve(request().selectors(selector));
294293

295294
assertTrue(engineDescriptor.getDescendants().isEmpty());
296-
assertThat(firstWarningLogRecord(listener).getMessage()) //
297-
.isEqualTo("UniqueIdSelector [uniqueId = " + uniqueId + "] could not be resolved.");
295+
assertUnresolved(selector);
298296
}
299297

300298
@Test
@@ -304,52 +302,47 @@ void resolvingUniqueIdOfNonTestMethodResolvesNothing() {
304302
resolve(request().selectors(selector));
305303

306304
assertThat(engineDescriptor.getDescendants()).isEmpty();
305+
assertUnresolved(selector);
307306
}
308307

309308
@Test
310-
@TrackLogRecords
311-
void methodResolutionByUniqueIdWithMissingMethodName(LogRecordListener listener) {
309+
void methodResolutionByUniqueIdWithMissingMethodName() {
312310
UniqueId uniqueId = uniqueIdForMethod(getClass(), "()");
313311

314312
resolve(request().selectors(selectUniqueId(uniqueId)));
315313

316314
assertTrue(engineDescriptor.getDescendants().isEmpty());
317-
LogRecord logRecord = firstWarningLogRecord(listener);
318-
assertThat(logRecord.getMessage()).isEqualTo(
319-
"UniqueIdSelector [uniqueId = " + uniqueId + "] could not be resolved.");
320-
assertThat(logRecord.getThrown())//
315+
var result = verifySelectorProcessed(selectUniqueId(uniqueId));
316+
assertThat(result.getStatus()).isEqualTo(FAILED);
317+
assertThat(result.getThrowable().get())//
321318
.isInstanceOf(PreconditionViolationException.class)//
322319
.hasMessageStartingWith("Method [()] does not match pattern");
323320
}
324321

325322
@Test
326-
@TrackLogRecords
327-
void methodResolutionByUniqueIdWithMissingParameters(LogRecordListener listener) {
323+
void methodResolutionByUniqueIdWithMissingParameters() {
328324
UniqueId uniqueId = uniqueIdForMethod(getClass(), "methodName");
329325

330326
resolve(request().selectors(selectUniqueId(uniqueId)));
331327

332328
assertThat(engineDescriptor.getDescendants()).isEmpty();
333-
LogRecord logRecord = firstWarningLogRecord(listener);
334-
assertThat(logRecord.getMessage()).isEqualTo(
335-
"UniqueIdSelector [uniqueId = " + uniqueId + "] could not be resolved.");
336-
assertThat(logRecord.getThrown())//
329+
var result = verifySelectorProcessed(selectUniqueId(uniqueId));
330+
assertThat(result.getStatus()).isEqualTo(FAILED);
331+
assertThat(result.getThrowable().get())//
337332
.isInstanceOf(PreconditionViolationException.class)//
338333
.hasMessageStartingWith("Method [methodName] does not match pattern");
339334
}
340335

341336
@Test
342-
@TrackLogRecords
343-
void methodResolutionByUniqueIdWithBogusParameters(LogRecordListener listener) {
337+
void methodResolutionByUniqueIdWithBogusParameters() {
344338
UniqueId uniqueId = uniqueIdForMethod(getClass(), "methodName(java.lang.String, junit.foo.Enigma)");
345339

346340
resolve(request().selectors(selectUniqueId(uniqueId)));
347341

348342
assertTrue(engineDescriptor.getDescendants().isEmpty());
349-
LogRecord logRecord = firstWarningLogRecord(listener);
350-
assertThat(logRecord.getMessage()).isEqualTo(
351-
"UniqueIdSelector [uniqueId = " + uniqueId + "] could not be resolved.");
352-
assertThat(logRecord.getThrown())//
343+
var result = verifySelectorProcessed(selectUniqueId(uniqueId));
344+
assertThat(result.getStatus()).isEqualTo(FAILED);
345+
assertThat(result.getThrowable().get())//
353346
.isInstanceOf(JUnitException.class)//
354347
.hasMessage("Failed to load parameter type [%s] for method [%s] in class [%s].", "junit.foo.Enigma",
355348
"methodName", getClass().getName());
@@ -381,8 +374,7 @@ void methodResolutionByUniqueIdFromInheritedClass() {
381374
}
382375

383376
@Test
384-
@TrackLogRecords
385-
void methodResolutionByUniqueIdWithParams(LogRecordListener listener) {
377+
void methodResolutionByUniqueIdWithParams() {
386378
UniqueIdSelector selector = selectUniqueId(
387379
uniqueIdForMethod(HerTestClass.class, "test7(java.lang.String)").toString());
388380

@@ -396,15 +388,13 @@ void methodResolutionByUniqueIdWithParams(LogRecordListener listener) {
396388
}
397389

398390
@Test
399-
@TrackLogRecords
400-
void resolvingUniqueIdWithWrongParamsResolvesNothing(LogRecordListener listener) {
391+
void resolvingUniqueIdWithWrongParamsResolvesNothing() {
401392
UniqueId uniqueId = uniqueIdForMethod(HerTestClass.class, "test7(java.math.BigDecimal)");
402393

403394
resolve(request().selectors(selectUniqueId(uniqueId)));
404395

405396
assertTrue(engineDescriptor.getDescendants().isEmpty());
406-
assertThat(firstWarningLogRecord(listener).getMessage())//
407-
.isEqualTo("UniqueIdSelector [uniqueId = " + uniqueId + "] could not be resolved.");
397+
assertUnresolved(selectUniqueId(uniqueId));
408398
}
409399

410400
@Test
@@ -633,8 +623,7 @@ void testTemplateMethodResolutionByUniqueId() {
633623
}
634624

635625
@Test
636-
@TrackLogRecords
637-
void resolvingDynamicTestByUniqueIdResolvesUpToParentTestFactory(LogRecordListener listener) {
626+
void resolvingDynamicTestByUniqueIdResolvesUpToParentTestFactory() {
638627
Class<?> clazz = MyTestClass.class;
639628
UniqueId factoryUid = uniqueIdForTestFactoryMethod(clazz, "dynamicTest()");
640629
UniqueId dynamicTestUid = factoryUid.append(DYNAMIC_TEST_SEGMENT_TYPE, "#1");
@@ -651,12 +640,11 @@ void resolvingDynamicTestByUniqueIdResolvesUpToParentTestFactory(LogRecordListen
651640
assertThat(dynamicDescendantFilter.test(dynamicTestUid)).isTrue();
652641
assertThat(dynamicDescendantFilter.test(differentDynamicTestUid)).isFalse();
653642

654-
assertZeroLogRecords(listener);
643+
assertAllSelectorsResolved();
655644
}
656645

657646
@Test
658-
@TrackLogRecords
659-
void resolvingDynamicContainerByUniqueIdResolvesUpToParentTestFactory(LogRecordListener listener) {
647+
void resolvingDynamicContainerByUniqueIdResolvesUpToParentTestFactory() {
660648
Class<?> clazz = MyTestClass.class;
661649
UniqueId factoryUid = uniqueIdForTestFactoryMethod(clazz, "dynamicTest()");
662650
UniqueId dynamicContainerUid = factoryUid.append(DYNAMIC_CONTAINER_SEGMENT_TYPE, "#1");
@@ -676,7 +664,7 @@ void resolvingDynamicContainerByUniqueIdResolvesUpToParentTestFactory(LogRecordL
676664
assertThat(dynamicDescendantFilter.test(differentDynamicContainerUid)).isFalse();
677665
assertThat(dynamicDescendantFilter.test(differentDynamicTestUid)).isFalse();
678666

679-
assertZeroLogRecords(listener);
667+
assertAllSelectorsResolved();
680668
}
681669

682670
@Test
@@ -765,18 +753,31 @@ private List<UniqueId> uniqueIds() {
765753
return engineDescriptor.getDescendants().stream().map(TestDescriptor::getUniqueId).collect(toList());
766754
}
767755

768-
private void assertZeroLogRecords(LogRecordListener listener) {
769-
assertThat(listener.stream(EngineDiscoveryRequestResolver.class)).isEmpty();
756+
private LauncherDiscoveryRequestBuilder request() {
757+
return LauncherDiscoveryRequestBuilder.request() //
758+
.configurationParameter(DEFAULT_DISCOVERY_LISTENER_CONFIGURATION_PROPERTY_NAME, "logging") //
759+
.listeners(discoveryListener);
770760
}
771761

772-
private LogRecord firstWarningLogRecord(LogRecordListener listener) throws AssertionError {
773-
return listener.stream(EngineDiscoveryRequestResolver.class, Level.WARNING).findFirst().orElseThrow(
774-
() -> new AssertionError("Failed to find warning log record"));
762+
private void assertAllSelectorsResolved() {
763+
ArgumentCaptor<SelectorResolutionResult> resultCaptor = ArgumentCaptor.forClass(SelectorResolutionResult.class);
764+
verify(discoveryListener).selectorProcessed(eq(UniqueId.forEngine("junit-jupiter")), any(),
765+
resultCaptor.capture());
766+
assertThat(resultCaptor.getAllValues()) //
767+
.flatExtracting(SelectorResolutionResult::getStatus) //
768+
.allMatch(Predicate.isEqual(RESOLVED));
775769
}
776770

777-
private LogRecord firstDebugLogRecord(LogRecordListener listener) throws AssertionError {
778-
return listener.stream(EngineDiscoveryRequestResolver.class, Level.FINE).findFirst().orElseThrow(
779-
() -> new AssertionError("Failed to find debug log record"));
771+
private void assertUnresolved(DiscoverySelector selector) {
772+
var result = verifySelectorProcessed(selector);
773+
assertThat(result.getStatus()).isEqualTo(UNRESOLVED);
774+
}
775+
776+
private SelectorResolutionResult verifySelectorProcessed(DiscoverySelector selector) {
777+
ArgumentCaptor<SelectorResolutionResult> resultCaptor = ArgumentCaptor.forClass(SelectorResolutionResult.class);
778+
verify(discoveryListener).selectorProcessed(eq(UniqueId.forEngine("junit-jupiter")), eq(selector),
779+
resultCaptor.capture());
780+
return resultCaptor.getValue();
780781
}
781782

782783
}

junit-jupiter-engine/src/test/resources/log4j2-test.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<Logger name="org.junit" level="warn" />
1010
<Logger name="org.junit.jupiter.api" level="off" />
1111
<Logger name="org.junit.jupiter.engine" level="off" />
12-
<Logger name="org.junit.platform.engine.support.discovery" level="off" />
12+
<Logger name="org.junit.platform.launcher.listeners.discovery.LoggingLauncherDiscoveryListener" level="off" />
1313
<Root level="error">
1414
<AppenderRef ref="Console" />
1515
</Root>

0 commit comments

Comments
 (0)