Skip to content

Commit 07c678d

Browse files
authored
Reuse Launcher orchestration in EngineTestKit (#2242)
Resolves #2109.
1 parent ab7e1be commit 07c678d

File tree

15 files changed

+581
-338
lines changed

15 files changed

+581
-338
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ on GitHub.
2222
* In the `EngineTestKit` API, the `all()`, `containers()`, and `tests()` methods in
2323
`EngineExecutionResults` that were deprecated in JUnit Platform 1.6.0 have been removed
2424
in favor of `allEvents()`, `containerEvents()`, and `testEvents()`, respectively.
25+
* The following methods in `EngineTestKit` are now deprecated with replacements:
26+
- `execute(String, EngineDiscoveryRequest)` → `execute(String, LauncherDiscoveryRequest)`
27+
- `execute(TestEngine, EngineDiscoveryRequest)` → `execute(TestEngine, LauncherDiscoveryRequest)`
28+
- `Builder.filters(DiscoveryFilter...)` → `Builder.filters(Filter...)`
2529

2630
==== New Features and Improvements
2731

@@ -36,6 +40,9 @@ on GitHub.
3640
execution of a submitted test via the returned `Future`.
3741
* Add `EngineExecutionListener.NOOP` and change all declared methods to have empty default
3842
implementations.
43+
* The `EngineTestKit` now reuses the same test discovery and execution logic as the
44+
`Launcher`. Thus, it's now possible to test that an engine's behavior in the presence of
45+
post-discovery filters (e.g. tag filters) and with regard to pruning.
3946
* The TestKit now allows to match conditions with events loosely, i.e. an incomplete match
4047
with or without a fixed order.
4148

documentation/src/docs/asciidoc/user-guide/testkit.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ given `{TestEngine}` running on the JUnit Platform and then accessing the result
1717
fluent API to verify the expected results. The key entry point into this API is the
1818
`{EngineTestKit}` which provides static factory methods named `engine()` and `execute()`.
1919
It is recommended that you select one of the `engine()` variants to benefit from the
20-
fluent API for building an `EngineDiscoveryRequest`.
20+
fluent API for building a `LauncherDiscoveryRequest`.
2121

2222
NOTE: If you prefer to use the `LauncherDiscoveryRequestBuilder` from the `Launcher` API
23-
to build your `EngineDiscoveryRequest`, you must use one of the `execute()` variants in
23+
to build your `LauncherDiscoveryRequest`, you must use one of the `execute()` variants in
2424
`EngineTestKit`.
2525

2626
The following test class written using JUnit Jupiter will be used in subsequent examples.

junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static org.assertj.core.api.Assertions.assertThat;
1414
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
15+
import static org.assertj.core.util.Throwables.getRootCause;
1516
import static org.junit.jupiter.api.Assertions.assertAll;
1617
import static org.junit.jupiter.api.Assertions.assertEquals;
1718
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
@@ -182,7 +183,7 @@ private void assertNestedCycle(Class<?> start, Class<?> from, Class<?> to) {
182183
assertThatExceptionOfType(JUnitException.class)//
183184
.isThrownBy(() -> executeTestsForClass(start))//
184185
.withCauseExactlyInstanceOf(JUnitException.class)//
185-
.satisfies(ex -> assertThat(ex.getCause()).hasMessageMatching(
186+
.satisfies(ex -> assertThat(getRootCause(ex)).hasMessageMatching(
186187
String.format("Detected cycle in inner class hierarchy between .+%s and .+%s", from.getSimpleName(),
187188
to.getSimpleName())));
188189
}

junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java

Lines changed: 8 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,9 @@
1010

1111
package org.junit.platform.launcher.core;
1212

13-
import java.util.HashSet;
14-
import java.util.Optional;
15-
import java.util.Set;
16-
import java.util.function.Consumer;
17-
18-
import org.junit.platform.commons.JUnitException;
19-
import org.junit.platform.commons.logging.Logger;
20-
import org.junit.platform.commons.logging.LoggerFactory;
21-
import org.junit.platform.commons.util.BlacklistedExceptions;
2213
import org.junit.platform.commons.util.Preconditions;
23-
import org.junit.platform.engine.ConfigurationParameters;
24-
import org.junit.platform.engine.EngineExecutionListener;
25-
import org.junit.platform.engine.ExecutionRequest;
26-
import org.junit.platform.engine.FilterResult;
27-
import org.junit.platform.engine.TestDescriptor;
2814
import org.junit.platform.engine.TestEngine;
29-
import org.junit.platform.engine.TestExecutionResult;
30-
import org.junit.platform.engine.UniqueId;
31-
import org.junit.platform.launcher.EngineDiscoveryResult;
3215
import org.junit.platform.launcher.Launcher;
33-
import org.junit.platform.launcher.LauncherDiscoveryListener;
3416
import org.junit.platform.launcher.LauncherDiscoveryRequest;
3517
import org.junit.platform.launcher.TestExecutionListener;
3618
import org.junit.platform.launcher.TestPlan;
@@ -47,11 +29,9 @@
4729
*/
4830
class DefaultLauncher implements Launcher {
4931

50-
private static final Logger logger = LoggerFactory.getLogger(DefaultLauncher.class);
51-
5232
private final TestExecutionListenerRegistry listenerRegistry = new TestExecutionListenerRegistry();
53-
private final EngineDiscoveryResultValidator discoveryResultValidator = new EngineDiscoveryResultValidator();
54-
private final Iterable<TestEngine> testEngines;
33+
private final EngineExecutionOrchestrator executionOrchestrator = new EngineExecutionOrchestrator(listenerRegistry);
34+
private final EngineDiscoveryOrchestrator discoveryOrchestrator;
5535

5636
/**
5737
* Construct a new {@code DefaultLauncher} with the supplied test engines.
@@ -62,53 +42,7 @@ class DefaultLauncher implements Launcher {
6242
Preconditions.condition(testEngines != null && testEngines.iterator().hasNext(),
6343
() -> "Cannot create Launcher without at least one TestEngine; "
6444
+ "consider adding an engine implementation JAR to the classpath");
65-
this.testEngines = validateEngineIds(testEngines);
66-
}
67-
68-
private static Iterable<TestEngine> validateEngineIds(Iterable<TestEngine> testEngines) {
69-
Set<String> ids = new HashSet<>();
70-
for (TestEngine testEngine : testEngines) {
71-
// check usage of reserved id prefix
72-
if (!validateReservedIds(testEngine)) {
73-
logger.warn(() -> String.format(
74-
"Third-party TestEngine implementations are forbidden to use the reserved 'junit-' prefix for their ID: '%s'",
75-
testEngine.getId()));
76-
}
77-
78-
// check uniqueness
79-
if (!ids.add(testEngine.getId())) {
80-
throw new JUnitException(String.format(
81-
"Cannot create Launcher for multiple engines with the same ID '%s'.", testEngine.getId()));
82-
}
83-
}
84-
return testEngines;
85-
}
86-
87-
// https://github.com/junit-team/junit5/issues/1557
88-
private static boolean validateReservedIds(TestEngine testEngine) {
89-
String engineId = testEngine.getId();
90-
if (!engineId.startsWith("junit-")) {
91-
return true;
92-
}
93-
if (engineId.equals("junit-jupiter")) {
94-
validateWellKnownClassName(testEngine, "org.junit.jupiter.engine.JupiterTestEngine");
95-
return true;
96-
}
97-
if (engineId.equals("junit-vintage")) {
98-
validateWellKnownClassName(testEngine, "org.junit.vintage.engine.VintageTestEngine");
99-
return true;
100-
}
101-
return false;
102-
}
103-
104-
private static void validateWellKnownClassName(TestEngine testEngine, String expectedClassName) {
105-
String actualClassName = testEngine.getClass().getName();
106-
if (actualClassName.equals(expectedClassName)) {
107-
return;
108-
}
109-
throw new JUnitException(
110-
String.format("Third-party TestEngine '%s' is forbidden to use the reserved '%s' TestEngine ID.",
111-
actualClassName, testEngine.getId()));
45+
this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(EngineIdValidator.validate(testEngines));
11246
}
11347

11448
@Override
@@ -121,15 +55,15 @@ public void registerTestExecutionListeners(TestExecutionListener... listeners) {
12155
@Override
12256
public TestPlan discover(LauncherDiscoveryRequest discoveryRequest) {
12357
Preconditions.notNull(discoveryRequest, "LauncherDiscoveryRequest must not be null");
124-
return InternalTestPlan.from(discoverRoot(discoveryRequest, "discovery"));
58+
return InternalTestPlan.from(discover(discoveryRequest, "discovery"));
12559
}
12660

12761
@Override
12862
public void execute(LauncherDiscoveryRequest discoveryRequest, TestExecutionListener... listeners) {
12963
Preconditions.notNull(discoveryRequest, "LauncherDiscoveryRequest must not be null");
13064
Preconditions.notNull(listeners, "TestExecutionListener array must not be null");
13165
Preconditions.containsNoNullElements(listeners, "individual listeners must not be null");
132-
execute(InternalTestPlan.from(discoverRoot(discoveryRequest, "execution")), listeners);
66+
execute(InternalTestPlan.from(discover(discoveryRequest, "execution")), listeners);
13367
}
13468

13569
@Override
@@ -145,114 +79,12 @@ TestExecutionListenerRegistry getTestExecutionListenerRegistry() {
14579
return listenerRegistry;
14680
}
14781

148-
private Root discoverRoot(LauncherDiscoveryRequest discoveryRequest, String phase) {
149-
Root root = new Root(discoveryRequest.getConfigurationParameters());
150-
151-
for (TestEngine testEngine : this.testEngines) {
152-
// @formatter:off
153-
boolean engineIsExcluded = discoveryRequest.getEngineFilters().stream()
154-
.map(engineFilter -> engineFilter.apply(testEngine))
155-
.anyMatch(FilterResult::excluded);
156-
// @formatter:on
157-
158-
if (engineIsExcluded) {
159-
logger.debug(() -> String.format(
160-
"Test discovery for engine '%s' was skipped due to an EngineFilter in phase '%s'.",
161-
testEngine.getId(), phase));
162-
continue;
163-
}
164-
165-
logger.debug(() -> String.format("Discovering tests during Launcher %s phase in engine '%s'.", phase,
166-
testEngine.getId()));
167-
168-
TestDescriptor rootDescriptor = discoverEngineRoot(testEngine, discoveryRequest);
169-
root.add(testEngine, rootDescriptor);
170-
}
171-
root.applyPostDiscoveryFilters(discoveryRequest);
172-
root.prune();
173-
return root;
174-
}
175-
176-
private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest) {
177-
LauncherDiscoveryListener discoveryListener = discoveryRequest.getDiscoveryListener();
178-
UniqueId uniqueEngineId = UniqueId.forEngine(testEngine.getId());
179-
try {
180-
discoveryListener.engineDiscoveryStarted(uniqueEngineId);
181-
TestDescriptor engineRoot = testEngine.discover(discoveryRequest, uniqueEngineId);
182-
discoveryResultValidator.validate(testEngine, engineRoot);
183-
discoveryListener.engineDiscoveryFinished(uniqueEngineId, EngineDiscoveryResult.successful());
184-
return engineRoot;
185-
}
186-
catch (Throwable throwable) {
187-
BlacklistedExceptions.rethrowIfBlacklisted(throwable);
188-
String message = String.format("TestEngine with ID '%s' failed to discover tests", testEngine.getId());
189-
JUnitException cause = new JUnitException(message, throwable);
190-
discoveryListener.engineDiscoveryFinished(uniqueEngineId, EngineDiscoveryResult.failed(cause));
191-
return new EngineDiscoveryErrorDescriptor(uniqueEngineId, testEngine, cause);
192-
}
82+
private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, String phase) {
83+
return discoveryOrchestrator.discover(discoveryRequest, phase);
19384
}
19485

19586
private void execute(InternalTestPlan internalTestPlan, TestExecutionListener[] listeners) {
196-
Root root = internalTestPlan.getRoot();
197-
ConfigurationParameters configurationParameters = root.getConfigurationParameters();
198-
TestExecutionListenerRegistry listenerRegistry = buildListenerRegistryForExecution(listeners);
199-
withInterceptedStreams(configurationParameters, listenerRegistry, testExecutionListener -> {
200-
testExecutionListener.testPlanExecutionStarted(internalTestPlan);
201-
ExecutionListenerAdapter engineExecutionListener = new ExecutionListenerAdapter(internalTestPlan,
202-
testExecutionListener);
203-
for (TestEngine testEngine : root.getTestEngines()) {
204-
TestDescriptor engineDescriptor = root.getTestDescriptorFor(testEngine);
205-
if (engineDescriptor instanceof EngineDiscoveryErrorDescriptor) {
206-
engineExecutionListener.executionStarted(engineDescriptor);
207-
engineExecutionListener.executionFinished(engineDescriptor,
208-
TestExecutionResult.failed(((EngineDiscoveryErrorDescriptor) engineDescriptor).getCause()));
209-
}
210-
else {
211-
execute(engineDescriptor, engineExecutionListener, configurationParameters, testEngine);
212-
}
213-
}
214-
testExecutionListener.testPlanExecutionFinished(internalTestPlan);
215-
});
216-
}
217-
218-
private void withInterceptedStreams(ConfigurationParameters configurationParameters,
219-
TestExecutionListenerRegistry listenerRegistry, Consumer<TestExecutionListener> action) {
220-
221-
TestExecutionListener testExecutionListener = listenerRegistry.getCompositeTestExecutionListener();
222-
Optional<StreamInterceptingTestExecutionListener> streamInterceptingTestExecutionListener = StreamInterceptingTestExecutionListener.create(
223-
configurationParameters, testExecutionListener::reportingEntryPublished);
224-
streamInterceptingTestExecutionListener.ifPresent(listenerRegistry::registerListeners);
225-
try {
226-
action.accept(testExecutionListener);
227-
}
228-
finally {
229-
streamInterceptingTestExecutionListener.ifPresent(StreamInterceptingTestExecutionListener::unregister);
230-
}
231-
}
232-
233-
private TestExecutionListenerRegistry buildListenerRegistryForExecution(TestExecutionListener... listeners) {
234-
if (listeners.length == 0) {
235-
return this.listenerRegistry;
236-
}
237-
TestExecutionListenerRegistry registry = new TestExecutionListenerRegistry(this.listenerRegistry);
238-
registry.registerListeners(listeners);
239-
return registry;
240-
}
241-
242-
private void execute(TestDescriptor engineDescriptor, EngineExecutionListener listener,
243-
ConfigurationParameters configurationParameters, TestEngine testEngine) {
244-
245-
OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener,
246-
engineDescriptor);
247-
try {
248-
testEngine.execute(new ExecutionRequest(engineDescriptor, delayingListener, configurationParameters));
249-
delayingListener.reportEngineOutcome();
250-
}
251-
catch (Throwable throwable) {
252-
BlacklistedExceptions.rethrowIfBlacklisted(throwable);
253-
delayingListener.reportEngineFailure(new JUnitException(
254-
String.format("TestEngine with ID '%s' failed to execute tests", testEngine.getId()), throwable));
255-
}
87+
executionOrchestrator.execute(internalTestPlan, listeners);
25688
}
25789

25890
}

0 commit comments

Comments
 (0)