Skip to content

Commit 8b71ee6

Browse files
filiphrmarcphilipp
andcommitted
Add possibility to register discovery listeners via SPI
`LauncherDiscoveryListeners` can now be registered via Java's `ServiceLoader` mechanism in addition to those passed as part of each `LauncherDiscoveryRequest` and the default one. Whether discovery listeners are picked up via ServiceLoader is configurable via `LauncherConfig` (defaults to true). Closes #2457. Related to #201. Co-authored-by: Marc Philipp <[email protected]>
1 parent 86d6645 commit 8b71ee6

File tree

19 files changed

+277
-43
lines changed

19 files changed

+277
-43
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ on GitHub.
3434
`my test`.
3535
* The `junit-platform-jfr` module now also reports events for containers, e.g. test
3636
classes.
37+
* Custom `LauncherDiscoveryListener` implementations can now be registered via Java’s
38+
`ServiceLoader` mechanism.
3739

3840

3941
[[release-notes-5.8.0-M1-junit-jupiter]]

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

+19-4
Original file line numberDiff line numberDiff line change
@@ -112,24 +112,39 @@ In addition to specifying post-discovery filters as part of a `{LauncherDiscover
112112
passed to the `{Launcher}` API, by default custom `{PostDiscoveryFilter}` implementations
113113
will be discovered at runtime via Java's `java.util.ServiceLoader` mechanism and
114114
automatically applied by the `Launcher` in addition to those that are part of the request.
115+
115116
For example, an `example.CustomTagFilter` class implementing `{PostDiscoveryFilter}` and
116117
declared within the `/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter`
117118
file is loaded and applied automatically.
118119

120+
[[launcher-api-launcher-discovery-listeners-custom]]
121+
==== Plugging in your own Discovery Listeners
122+
123+
In addition to specifying post-discovery filters as part of a `{LauncherDiscoveryRequest}`
124+
passed to the `{Launcher}` API, custom `{LauncherDiscoveryListener}` implementations can
125+
be discovered at runtime via Java's `java.util.ServiceLoader` mechanism and automatically
126+
automatically registered with the `Launcher` created via the `LauncherFactory`.
127+
128+
For example, an `example.CustomLauncherDiscoveryListener` class implementing
129+
`{LauncherDiscoveryListener}` and declared within the
130+
`/META-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener` file is loaded
131+
and applied automatically.
132+
119133
[[launcher-api-listeners-custom]]
120-
==== Plugging in your own Test Execution Listener
134+
==== Plugging in your own Execution Listener
121135

122136
In addition to the public `{Launcher}` API method for registering test execution
123137
listeners programmatically, by default custom `{TestExecutionListener}` implementations
124138
will be discovered at runtime via Java's `java.util.ServiceLoader` mechanism and
125-
automatically registered with the `Launcher` created via the `LauncherFactory`. For
126-
example, an `example.TestInfoPrinter` class implementing `{TestExecutionListener}` and
139+
automatically registered with the `Launcher` created via the `LauncherFactory`.
140+
141+
For example, an `example.TestInfoPrinter` class implementing `{TestExecutionListener}` and
127142
declared within the
128143
`/META-INF/services/org.junit.platform.launcher.TestExecutionListener` file is loaded and
129144
registered automatically.
130145

131146
[[launcher-api-listeners-custom-deactivation]]
132-
==== Deactivating Test Execution Listeners
147+
==== Deactivating Execution Listeners
133148

134149
Sometimes it can be useful to run a test suite _without_ certain execution listeners being
135150
active. For example, you might have custom a `TestExecutionListener` that sends the test

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

+17-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.junit.platform.commons.util.Preconditions;
1818
import org.junit.platform.engine.TestEngine;
1919
import org.junit.platform.launcher.Launcher;
20+
import org.junit.platform.launcher.LauncherDiscoveryListener;
2021
import org.junit.platform.launcher.LauncherDiscoveryRequest;
2122
import org.junit.platform.launcher.PostDiscoveryFilter;
2223
import org.junit.platform.launcher.TestExecutionListener;
@@ -41,17 +42,23 @@ class DefaultLauncher implements Launcher {
4142
/**
4243
* Construct a new {@code DefaultLauncher} with the supplied test engines.
4344
*
44-
* @param testEngines the test engines to delegate to; never {@code null} or empty
45-
* @param filters the additional post discovery filters for discovery requests; never {@code null}
45+
* @param testEngines the test engines to delegate to; never {@code null} or
46+
* empty
47+
* @param postDiscoveryFilters the additional post discovery filters for
48+
* discovery requests; never {@code null}
49+
* @param launcherDiscoveryListeners the additional launcher discovery
50+
* listeners for discovery requests; never {@code null}
4651
*/
47-
DefaultLauncher(Iterable<TestEngine> testEngines, Collection<PostDiscoveryFilter> filters) {
52+
DefaultLauncher(Iterable<TestEngine> testEngines, Collection<PostDiscoveryFilter> postDiscoveryFilters,
53+
Collection<LauncherDiscoveryListener> launcherDiscoveryListeners) {
4854
Preconditions.condition(testEngines != null && testEngines.iterator().hasNext(),
4955
() -> "Cannot create Launcher without at least one TestEngine; "
5056
+ "consider adding an engine implementation JAR to the classpath");
51-
Preconditions.notNull(filters, "PostDiscoveryFilter array must not be null");
52-
Preconditions.containsNoNullElements(filters, "PostDiscoveryFilter array must not contain null elements");
57+
Preconditions.notNull(postDiscoveryFilters, "PostDiscoveryFilter array must not be null");
58+
Preconditions.containsNoNullElements(postDiscoveryFilters,
59+
"PostDiscoveryFilter array must not contain null elements");
5360
this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(EngineIdValidator.validate(testEngines),
54-
unmodifiableCollection(filters));
61+
unmodifiableCollection(postDiscoveryFilters), unmodifiableCollection(launcherDiscoveryListeners));
5562
}
5663

5764
@Override
@@ -88,6 +95,10 @@ TestExecutionListenerRegistry getTestExecutionListenerRegistry() {
8895
return listenerRegistry;
8996
}
9097

98+
LauncherDiscoveryListener getLauncherDiscoveryListener(LauncherDiscoveryRequest discoveryRequest) {
99+
return discoveryOrchestrator.getLauncherDiscoveryListener(discoveryRequest);
100+
}
101+
91102
private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, String phase) {
92103
return discoveryOrchestrator.discover(discoveryRequest, phase);
93104
}

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,20 @@
2626
class DefaultLauncherConfig implements LauncherConfig {
2727

2828
private final boolean testEngineAutoRegistrationEnabled;
29-
29+
private final boolean launcherDiscoveryListenerAutoRegistrationEnabled;
3030
private final boolean testExecutionListenerAutoRegistrationEnabled;
31-
3231
private final boolean postDiscoveryFilterAutoRegistrationEnabled;
33-
3432
private final Collection<TestEngine> additionalTestEngines;
35-
3633
private final Collection<TestExecutionListener> additionalTestExecutionListeners;
37-
3834
private final Collection<PostDiscoveryFilter> additionalPostDiscoveryFilters;
3935

4036
DefaultLauncherConfig(boolean testEngineAutoRegistrationEnabled,
37+
boolean launcherDiscoveryListenerAutoRegistrationEnabled,
4138
boolean testExecutionListenerAutoRegistrationEnabled, boolean postDiscoveryFilterAutoRegistrationEnabled,
4239
Collection<TestEngine> additionalTestEngines,
4340
Collection<TestExecutionListener> additionalTestExecutionListeners,
4441
Collection<PostDiscoveryFilter> additionalPostDiscoveryFilters) {
45-
42+
this.launcherDiscoveryListenerAutoRegistrationEnabled = launcherDiscoveryListenerAutoRegistrationEnabled;
4643
this.testExecutionListenerAutoRegistrationEnabled = testExecutionListenerAutoRegistrationEnabled;
4744
this.testEngineAutoRegistrationEnabled = testEngineAutoRegistrationEnabled;
4845
this.postDiscoveryFilterAutoRegistrationEnabled = postDiscoveryFilterAutoRegistrationEnabled;
@@ -56,6 +53,11 @@ public boolean isTestEngineAutoRegistrationEnabled() {
5653
return this.testEngineAutoRegistrationEnabled;
5754
}
5855

56+
@Override
57+
public boolean isLauncherDiscoveryListenerAutoRegistrationEnabled() {
58+
return launcherDiscoveryListenerAutoRegistrationEnabled;
59+
}
60+
5961
@Override
6062
public boolean isTestExecutionListenerAutoRegistrationEnabled() {
6163
return this.testExecutionListenerAutoRegistrationEnabled;

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

+18-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package org.junit.platform.launcher.core;
1212

1313
import static java.util.stream.Collectors.joining;
14+
import static java.util.stream.Collectors.toList;
1415
import static org.apiguardian.api.API.Status.INTERNAL;
1516
import static org.junit.platform.engine.Filter.composeFilters;
1617

@@ -20,6 +21,7 @@
2021
import java.util.List;
2122
import java.util.Map;
2223
import java.util.Optional;
24+
import java.util.stream.Stream;
2325

2426
import org.apiguardian.api.API;
2527
import org.junit.platform.commons.JUnitException;
@@ -35,6 +37,7 @@
3537
import org.junit.platform.launcher.LauncherDiscoveryListener;
3638
import org.junit.platform.launcher.LauncherDiscoveryRequest;
3739
import org.junit.platform.launcher.PostDiscoveryFilter;
40+
import org.junit.platform.launcher.listeners.discovery.LauncherDiscoveryListeners;
3841

3942
/**
4043
* Orchestrates test discovery using the configured test engines.
@@ -49,11 +52,14 @@ public class EngineDiscoveryOrchestrator {
4952
private final EngineDiscoveryResultValidator discoveryResultValidator = new EngineDiscoveryResultValidator();
5053
private final Iterable<TestEngine> testEngines;
5154
private final Collection<PostDiscoveryFilter> postDiscoveryFilters;
55+
private final Collection<LauncherDiscoveryListener> launcherDiscoveryListeners;
5256

5357
public EngineDiscoveryOrchestrator(Iterable<TestEngine> testEngines,
54-
Collection<PostDiscoveryFilter> postDiscoveryFilters) {
58+
Collection<PostDiscoveryFilter> postDiscoveryFilters,
59+
Collection<LauncherDiscoveryListener> launcherDiscoveryListeners) {
5560
this.testEngines = testEngines;
5661
this.postDiscoveryFilters = postDiscoveryFilters;
62+
this.launcherDiscoveryListeners = launcherDiscoveryListeners;
5763
}
5864

5965
/**
@@ -96,7 +102,7 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, String
96102
}
97103

98104
private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest) {
99-
LauncherDiscoveryListener discoveryListener = discoveryRequest.getDiscoveryListener();
105+
LauncherDiscoveryListener discoveryListener = getLauncherDiscoveryListener(discoveryRequest);
100106
UniqueId uniqueEngineId = UniqueId.forEngine(testEngine.getId());
101107
try {
102108
discoveryListener.engineDiscoveryStarted(uniqueEngineId);
@@ -114,6 +120,16 @@ private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscove
114120
}
115121
}
116122

123+
LauncherDiscoveryListener getLauncherDiscoveryListener(LauncherDiscoveryRequest discoveryRequest) {
124+
LauncherDiscoveryListener discoveryListener = discoveryRequest.getDiscoveryListener();
125+
if (!launcherDiscoveryListeners.isEmpty()) {
126+
List<LauncherDiscoveryListener> allDiscoveryListeners = Stream.concat(Stream.of(discoveryListener),
127+
launcherDiscoveryListeners.stream()).collect(toList());
128+
discoveryListener = LauncherDiscoveryListeners.composite(allDiscoveryListeners);
129+
}
130+
return discoveryListener;
131+
}
132+
117133
private void applyPostDiscoveryFilters(Map<TestEngine, TestDescriptor> testEngineDescriptors,
118134
List<PostDiscoveryFilter> filters) {
119135
Filter<TestDescriptor> postDiscoveryFilter = composeFilters(filters);

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

+40-10
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ public interface LauncherConfig {
6060
*/
6161
boolean isTestEngineAutoRegistrationEnabled();
6262

63+
/**
64+
* Determine if launcher discovery listeners should be discovered at runtime
65+
* using the {@link java.util.ServiceLoader ServiceLoader} mechanism and
66+
* automatically registered.
67+
*
68+
* @return {@code true} if launcher discovery listeners should be
69+
* automatically registered
70+
* @since 1.8
71+
*/
72+
@API(status = EXPERIMENTAL, since = "1.8")
73+
boolean isLauncherDiscoveryListenerAutoRegistrationEnabled();
74+
6375
/**
6476
* Determine if test execution listeners should be discovered at runtime
6577
* using the {@link java.util.ServiceLoader ServiceLoader} mechanism and
@@ -77,6 +89,7 @@ public interface LauncherConfig {
7789
*
7890
* @return {@code true} if post discovery filters should be automatically
7991
* registered
92+
* @since 1.7
8093
*/
8194
@API(status = EXPERIMENTAL, since = "1.7")
8295
boolean isPostDiscoveryFilterAutoRegistrationEnabled();
@@ -105,6 +118,7 @@ public interface LauncherConfig {
105118
*
106119
* @return the collection of additional post discovery filters; never
107120
* {@code null} but potentially empty
121+
* @since 1.7
108122
*/
109123
@API(status = EXPERIMENTAL, since = "1.7")
110124
Collection<PostDiscoveryFilter> getAdditionalPostDiscoveryFilters();
@@ -123,22 +137,35 @@ static Builder builder() {
123137
*/
124138
class Builder {
125139

126-
private boolean listenerAutoRegistrationEnabled = true;
127-
128140
private boolean engineAutoRegistrationEnabled = true;
129-
141+
private boolean launcherDiscoveryListenerAutoRegistrationEnabled = true;
142+
private boolean testExecutionListenerAutoRegistrationEnabled = true;
130143
private boolean postDiscoveryFilterAutoRegistrationEnabled = true;
131-
132144
private final Collection<TestEngine> engines = new LinkedHashSet<>();
133-
134145
private final Collection<TestExecutionListener> listeners = new LinkedHashSet<>();
135-
136146
private final Collection<PostDiscoveryFilter> postDiscoveryFilters = new LinkedHashSet<>();
137147

138148
private Builder() {
139149
/* no-op */
140150
}
141151

152+
/**
153+
* Configure the auto-registration flag for launcher discovery
154+
* listeners.
155+
*
156+
* <p>Defaults to {@code true}.
157+
*
158+
* @param enabled {@code true} if launcher discovery listeners should be
159+
* automatically registered
160+
* @return this builder for method chaining
161+
* @since 1.8
162+
*/
163+
@API(status = EXPERIMENTAL, since = "1.8")
164+
public Builder enableLauncherDiscoveryListenerAutoRegistration(boolean enabled) {
165+
this.launcherDiscoveryListenerAutoRegistrationEnabled = enabled;
166+
return this;
167+
}
168+
142169
/**
143170
* Configure the auto-registration flag for test execution listeners.
144171
*
@@ -149,7 +176,7 @@ private Builder() {
149176
* @return this builder for method chaining
150177
*/
151178
public Builder enableTestExecutionListenerAutoRegistration(boolean enabled) {
152-
this.listenerAutoRegistrationEnabled = enabled;
179+
this.testExecutionListenerAutoRegistrationEnabled = enabled;
153180
return this;
154181
}
155182

@@ -175,6 +202,7 @@ public Builder enableTestEngineAutoRegistration(boolean enabled) {
175202
* @param enabled {@code true} if post discovery filters should be automatically
176203
* registered
177204
* @return this builder for method chaining
205+
* @since 1.7
178206
*/
179207
@API(status = EXPERIMENTAL, since = "1.7")
180208
public Builder enablePostDiscoveryFilterAutoRegistration(boolean enabled) {
@@ -217,6 +245,7 @@ public Builder addTestExecutionListeners(TestExecutionListener... listeners) {
217245
* @param filters additional post discovery filters to register;
218246
* never {@code null} or containing {@code null}
219247
* @return this builder for method chaining
248+
* @since 1.7
220249
*/
221250
@API(status = EXPERIMENTAL, since = "1.7")
222251
public Builder addPostDiscoveryFilters(PostDiscoveryFilter... filters) {
@@ -231,9 +260,10 @@ public Builder addPostDiscoveryFilters(PostDiscoveryFilter... filters) {
231260
* builder.
232261
*/
233262
public LauncherConfig build() {
234-
return new DefaultLauncherConfig(this.engineAutoRegistrationEnabled, this.listenerAutoRegistrationEnabled,
235-
this.postDiscoveryFilterAutoRegistrationEnabled, this.engines, this.listeners,
236-
this.postDiscoveryFilters);
263+
return new DefaultLauncherConfig(this.engineAutoRegistrationEnabled,
264+
this.launcherDiscoveryListenerAutoRegistrationEnabled,
265+
this.testExecutionListenerAutoRegistrationEnabled, this.postDiscoveryFilterAutoRegistrationEnabled,
266+
this.engines, this.listeners, this.postDiscoveryFilters);
237267
}
238268

239269
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,8 @@ private LauncherDiscoveryListener getLauncherDiscoveryListener(ConfigurationPara
274274
if (discoveryListeners.contains(defaultDiscoveryListener)) {
275275
return LauncherDiscoveryListeners.composite(discoveryListeners);
276276
}
277-
List<LauncherDiscoveryListener> allDiscoveryListeners = new ArrayList<>(discoveryListeners);
277+
List<LauncherDiscoveryListener> allDiscoveryListeners = new ArrayList<>(discoveryListeners.size() + 1);
278+
allDiscoveryListeners.addAll(discoveryListeners);
278279
allDiscoveryListeners.add(defaultDiscoveryListener);
279280
return LauncherDiscoveryListeners.composite(allDiscoveryListeners);
280281
}

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

+26-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.junit.platform.engine.ConfigurationParameters;
2929
import org.junit.platform.engine.TestEngine;
3030
import org.junit.platform.launcher.Launcher;
31+
import org.junit.platform.launcher.LauncherDiscoveryListener;
3132
import org.junit.platform.launcher.PostDiscoveryFilter;
3233
import org.junit.platform.launcher.TestExecutionListener;
3334

@@ -90,26 +91,48 @@ public static Launcher create() throws PreconditionViolationException {
9091
public static Launcher create(LauncherConfig config) throws PreconditionViolationException {
9192
Preconditions.notNull(config, "LauncherConfig must not be null");
9293

94+
Set<TestEngine> engines = collectTestEngines(config);
95+
List<PostDiscoveryFilter> filters = collectPostDiscoveryFilters(config);
96+
List<LauncherDiscoveryListener> discoveryListeners = collectLauncherDiscoveryListeners(config);
97+
98+
Launcher launcher = new DefaultLauncher(engines, filters, discoveryListeners);
99+
100+
registerTestExecutionListeners(config, launcher);
101+
102+
return launcher;
103+
}
104+
105+
private static Set<TestEngine> collectTestEngines(LauncherConfig config) {
93106
Set<TestEngine> engines = new LinkedHashSet<>();
94107
if (config.isTestEngineAutoRegistrationEnabled()) {
95108
new ServiceLoaderTestEngineRegistry().loadTestEngines().forEach(engines::add);
96109
}
97110
engines.addAll(config.getAdditionalTestEngines());
111+
return engines;
112+
}
98113

114+
private static List<PostDiscoveryFilter> collectPostDiscoveryFilters(LauncherConfig config) {
99115
List<PostDiscoveryFilter> filters = new ArrayList<>();
100116
if (config.isPostDiscoveryFilterAutoRegistrationEnabled()) {
101117
new ServiceLoaderPostDiscoveryFilterRegistry().loadPostDiscoveryFilters().forEach(filters::add);
102118
}
103119
filters.addAll(config.getAdditionalPostDiscoveryFilters());
120+
return filters;
121+
}
104122

105-
Launcher launcher = new DefaultLauncher(engines, filters);
123+
private static List<LauncherDiscoveryListener> collectLauncherDiscoveryListeners(LauncherConfig config) {
124+
List<LauncherDiscoveryListener> discoveryListeners = new ArrayList<>();
125+
if (config.isLauncherDiscoveryListenerAutoRegistrationEnabled()) {
126+
new ServiceLoaderLauncherDiscoveryListenerRegistry().loadListeners().forEach(discoveryListeners::add);
127+
}
128+
return discoveryListeners;
129+
}
106130

131+
private static void registerTestExecutionListeners(LauncherConfig config, Launcher launcher) {
107132
if (config.isTestExecutionListenerAutoRegistrationEnabled()) {
108133
loadAndFilterTestExecutionListeners().forEach(launcher::registerTestExecutionListeners);
109134
}
110135
config.getAdditionalTestExecutionListeners().forEach(launcher::registerTestExecutionListeners);
111-
112-
return launcher;
113136
}
114137

115138
private static Stream<TestExecutionListener> loadAndFilterTestExecutionListeners() {

0 commit comments

Comments
 (0)