Skip to content

Commit

Permalink
Add possibility to register discovery listeners via SPI
Browse files Browse the repository at this point in the history
`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]>
  • Loading branch information
filiphr and marcphilipp committed Dec 21, 2020
1 parent 86d6645 commit 8b71ee6
Show file tree
Hide file tree
Showing 19 changed files with 277 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ on GitHub.
`my test`.
* The `junit-platform-jfr` module now also reports events for containers, e.g. test
classes.
* Custom `LauncherDiscoveryListener` implementations can now be registered via Java’s
`ServiceLoader` mechanism.


[[release-notes-5.8.0-M1-junit-jupiter]]
Expand Down
23 changes: 19 additions & 4 deletions documentation/src/docs/asciidoc/user-guide/launcher-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,39 @@ In addition to specifying post-discovery filters as part of a `{LauncherDiscover
passed to the `{Launcher}` API, by default custom `{PostDiscoveryFilter}` implementations
will be discovered at runtime via Java's `java.util.ServiceLoader` mechanism and
automatically applied by the `Launcher` in addition to those that are part of the request.

For example, an `example.CustomTagFilter` class implementing `{PostDiscoveryFilter}` and
declared within the `/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter`
file is loaded and applied automatically.

[[launcher-api-launcher-discovery-listeners-custom]]
==== Plugging in your own Discovery Listeners

In addition to specifying post-discovery filters as part of a `{LauncherDiscoveryRequest}`
passed to the `{Launcher}` API, custom `{LauncherDiscoveryListener}` implementations can
be discovered at runtime via Java's `java.util.ServiceLoader` mechanism and automatically
automatically registered with the `Launcher` created via the `LauncherFactory`.

For example, an `example.CustomLauncherDiscoveryListener` class implementing
`{LauncherDiscoveryListener}` and declared within the
`/META-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener` file is loaded
and applied automatically.

[[launcher-api-listeners-custom]]
==== Plugging in your own Test Execution Listener
==== Plugging in your own Execution Listener

In addition to the public `{Launcher}` API method for registering test execution
listeners programmatically, by default custom `{TestExecutionListener}` implementations
will be discovered at runtime via Java's `java.util.ServiceLoader` mechanism and
automatically registered with the `Launcher` created via the `LauncherFactory`. For
example, an `example.TestInfoPrinter` class implementing `{TestExecutionListener}` and
automatically registered with the `Launcher` created via the `LauncherFactory`.

For example, an `example.TestInfoPrinter` class implementing `{TestExecutionListener}` and
declared within the
`/META-INF/services/org.junit.platform.launcher.TestExecutionListener` file is loaded and
registered automatically.

[[launcher-api-listeners-custom-deactivation]]
==== Deactivating Test Execution Listeners
==== Deactivating Execution Listeners

Sometimes it can be useful to run a test suite _without_ certain execution listeners being
active. For example, you might have custom a `TestExecutionListener` that sends the test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.TestExecutionListener;
Expand All @@ -41,17 +42,23 @@ class DefaultLauncher implements Launcher {
/**
* Construct a new {@code DefaultLauncher} with the supplied test engines.
*
* @param testEngines the test engines to delegate to; never {@code null} or empty
* @param filters the additional post discovery filters for discovery requests; never {@code null}
* @param testEngines the test engines to delegate to; never {@code null} or
* empty
* @param postDiscoveryFilters the additional post discovery filters for
* discovery requests; never {@code null}
* @param launcherDiscoveryListeners the additional launcher discovery
* listeners for discovery requests; never {@code null}
*/
DefaultLauncher(Iterable<TestEngine> testEngines, Collection<PostDiscoveryFilter> filters) {
DefaultLauncher(Iterable<TestEngine> testEngines, Collection<PostDiscoveryFilter> postDiscoveryFilters,
Collection<LauncherDiscoveryListener> launcherDiscoveryListeners) {
Preconditions.condition(testEngines != null && testEngines.iterator().hasNext(),
() -> "Cannot create Launcher without at least one TestEngine; "
+ "consider adding an engine implementation JAR to the classpath");
Preconditions.notNull(filters, "PostDiscoveryFilter array must not be null");
Preconditions.containsNoNullElements(filters, "PostDiscoveryFilter array must not contain null elements");
Preconditions.notNull(postDiscoveryFilters, "PostDiscoveryFilter array must not be null");
Preconditions.containsNoNullElements(postDiscoveryFilters,
"PostDiscoveryFilter array must not contain null elements");
this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(EngineIdValidator.validate(testEngines),
unmodifiableCollection(filters));
unmodifiableCollection(postDiscoveryFilters), unmodifiableCollection(launcherDiscoveryListeners));
}

@Override
Expand Down Expand Up @@ -88,6 +95,10 @@ TestExecutionListenerRegistry getTestExecutionListenerRegistry() {
return listenerRegistry;
}

LauncherDiscoveryListener getLauncherDiscoveryListener(LauncherDiscoveryRequest discoveryRequest) {
return discoveryOrchestrator.getLauncherDiscoveryListener(discoveryRequest);
}

private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, String phase) {
return discoveryOrchestrator.discover(discoveryRequest, phase);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,20 @@
class DefaultLauncherConfig implements LauncherConfig {

private final boolean testEngineAutoRegistrationEnabled;

private final boolean launcherDiscoveryListenerAutoRegistrationEnabled;
private final boolean testExecutionListenerAutoRegistrationEnabled;

private final boolean postDiscoveryFilterAutoRegistrationEnabled;

private final Collection<TestEngine> additionalTestEngines;

private final Collection<TestExecutionListener> additionalTestExecutionListeners;

private final Collection<PostDiscoveryFilter> additionalPostDiscoveryFilters;

DefaultLauncherConfig(boolean testEngineAutoRegistrationEnabled,
boolean launcherDiscoveryListenerAutoRegistrationEnabled,
boolean testExecutionListenerAutoRegistrationEnabled, boolean postDiscoveryFilterAutoRegistrationEnabled,
Collection<TestEngine> additionalTestEngines,
Collection<TestExecutionListener> additionalTestExecutionListeners,
Collection<PostDiscoveryFilter> additionalPostDiscoveryFilters) {

this.launcherDiscoveryListenerAutoRegistrationEnabled = launcherDiscoveryListenerAutoRegistrationEnabled;
this.testExecutionListenerAutoRegistrationEnabled = testExecutionListenerAutoRegistrationEnabled;
this.testEngineAutoRegistrationEnabled = testEngineAutoRegistrationEnabled;
this.postDiscoveryFilterAutoRegistrationEnabled = postDiscoveryFilterAutoRegistrationEnabled;
Expand All @@ -56,6 +53,11 @@ public boolean isTestEngineAutoRegistrationEnabled() {
return this.testEngineAutoRegistrationEnabled;
}

@Override
public boolean isLauncherDiscoveryListenerAutoRegistrationEnabled() {
return launcherDiscoveryListenerAutoRegistrationEnabled;
}

@Override
public boolean isTestExecutionListenerAutoRegistrationEnabled() {
return this.testExecutionListenerAutoRegistrationEnabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package org.junit.platform.launcher.core;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.platform.engine.Filter.composeFilters;

Expand All @@ -20,6 +21,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

import org.apiguardian.api.API;
import org.junit.platform.commons.JUnitException;
Expand All @@ -35,6 +37,7 @@
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.listeners.discovery.LauncherDiscoveryListeners;

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

public EngineDiscoveryOrchestrator(Iterable<TestEngine> testEngines,
Collection<PostDiscoveryFilter> postDiscoveryFilters) {
Collection<PostDiscoveryFilter> postDiscoveryFilters,
Collection<LauncherDiscoveryListener> launcherDiscoveryListeners) {
this.testEngines = testEngines;
this.postDiscoveryFilters = postDiscoveryFilters;
this.launcherDiscoveryListeners = launcherDiscoveryListeners;
}

/**
Expand Down Expand Up @@ -96,7 +102,7 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, String
}

private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest) {
LauncherDiscoveryListener discoveryListener = discoveryRequest.getDiscoveryListener();
LauncherDiscoveryListener discoveryListener = getLauncherDiscoveryListener(discoveryRequest);
UniqueId uniqueEngineId = UniqueId.forEngine(testEngine.getId());
try {
discoveryListener.engineDiscoveryStarted(uniqueEngineId);
Expand All @@ -114,6 +120,16 @@ private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscove
}
}

LauncherDiscoveryListener getLauncherDiscoveryListener(LauncherDiscoveryRequest discoveryRequest) {
LauncherDiscoveryListener discoveryListener = discoveryRequest.getDiscoveryListener();
if (!launcherDiscoveryListeners.isEmpty()) {
List<LauncherDiscoveryListener> allDiscoveryListeners = Stream.concat(Stream.of(discoveryListener),
launcherDiscoveryListeners.stream()).collect(toList());
discoveryListener = LauncherDiscoveryListeners.composite(allDiscoveryListeners);
}
return discoveryListener;
}

private void applyPostDiscoveryFilters(Map<TestEngine, TestDescriptor> testEngineDescriptors,
List<PostDiscoveryFilter> filters) {
Filter<TestDescriptor> postDiscoveryFilter = composeFilters(filters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ public interface LauncherConfig {
*/
boolean isTestEngineAutoRegistrationEnabled();

/**
* Determine if launcher discovery listeners should be discovered at runtime
* using the {@link java.util.ServiceLoader ServiceLoader} mechanism and
* automatically registered.
*
* @return {@code true} if launcher discovery listeners should be
* automatically registered
* @since 1.8
*/
@API(status = EXPERIMENTAL, since = "1.8")
boolean isLauncherDiscoveryListenerAutoRegistrationEnabled();

/**
* Determine if test execution listeners should be discovered at runtime
* using the {@link java.util.ServiceLoader ServiceLoader} mechanism and
Expand All @@ -77,6 +89,7 @@ public interface LauncherConfig {
*
* @return {@code true} if post discovery filters should be automatically
* registered
* @since 1.7
*/
@API(status = EXPERIMENTAL, since = "1.7")
boolean isPostDiscoveryFilterAutoRegistrationEnabled();
Expand Down Expand Up @@ -105,6 +118,7 @@ public interface LauncherConfig {
*
* @return the collection of additional post discovery filters; never
* {@code null} but potentially empty
* @since 1.7
*/
@API(status = EXPERIMENTAL, since = "1.7")
Collection<PostDiscoveryFilter> getAdditionalPostDiscoveryFilters();
Expand All @@ -123,22 +137,35 @@ static Builder builder() {
*/
class Builder {

private boolean listenerAutoRegistrationEnabled = true;

private boolean engineAutoRegistrationEnabled = true;

private boolean launcherDiscoveryListenerAutoRegistrationEnabled = true;
private boolean testExecutionListenerAutoRegistrationEnabled = true;
private boolean postDiscoveryFilterAutoRegistrationEnabled = true;

private final Collection<TestEngine> engines = new LinkedHashSet<>();

private final Collection<TestExecutionListener> listeners = new LinkedHashSet<>();

private final Collection<PostDiscoveryFilter> postDiscoveryFilters = new LinkedHashSet<>();

private Builder() {
/* no-op */
}

/**
* Configure the auto-registration flag for launcher discovery
* listeners.
*
* <p>Defaults to {@code true}.
*
* @param enabled {@code true} if launcher discovery listeners should be
* automatically registered
* @return this builder for method chaining
* @since 1.8
*/
@API(status = EXPERIMENTAL, since = "1.8")
public Builder enableLauncherDiscoveryListenerAutoRegistration(boolean enabled) {
this.launcherDiscoveryListenerAutoRegistrationEnabled = enabled;
return this;
}

/**
* Configure the auto-registration flag for test execution listeners.
*
Expand All @@ -149,7 +176,7 @@ private Builder() {
* @return this builder for method chaining
*/
public Builder enableTestExecutionListenerAutoRegistration(boolean enabled) {
this.listenerAutoRegistrationEnabled = enabled;
this.testExecutionListenerAutoRegistrationEnabled = enabled;
return this;
}

Expand All @@ -175,6 +202,7 @@ public Builder enableTestEngineAutoRegistration(boolean enabled) {
* @param enabled {@code true} if post discovery filters should be automatically
* registered
* @return this builder for method chaining
* @since 1.7
*/
@API(status = EXPERIMENTAL, since = "1.7")
public Builder enablePostDiscoveryFilterAutoRegistration(boolean enabled) {
Expand Down Expand Up @@ -217,6 +245,7 @@ public Builder addTestExecutionListeners(TestExecutionListener... listeners) {
* @param filters additional post discovery filters to register;
* never {@code null} or containing {@code null}
* @return this builder for method chaining
* @since 1.7
*/
@API(status = EXPERIMENTAL, since = "1.7")
public Builder addPostDiscoveryFilters(PostDiscoveryFilter... filters) {
Expand All @@ -231,9 +260,10 @@ public Builder addPostDiscoveryFilters(PostDiscoveryFilter... filters) {
* builder.
*/
public LauncherConfig build() {
return new DefaultLauncherConfig(this.engineAutoRegistrationEnabled, this.listenerAutoRegistrationEnabled,
this.postDiscoveryFilterAutoRegistrationEnabled, this.engines, this.listeners,
this.postDiscoveryFilters);
return new DefaultLauncherConfig(this.engineAutoRegistrationEnabled,
this.launcherDiscoveryListenerAutoRegistrationEnabled,
this.testExecutionListenerAutoRegistrationEnabled, this.postDiscoveryFilterAutoRegistrationEnabled,
this.engines, this.listeners, this.postDiscoveryFilters);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ private LauncherDiscoveryListener getLauncherDiscoveryListener(ConfigurationPara
if (discoveryListeners.contains(defaultDiscoveryListener)) {
return LauncherDiscoveryListeners.composite(discoveryListeners);
}
List<LauncherDiscoveryListener> allDiscoveryListeners = new ArrayList<>(discoveryListeners);
List<LauncherDiscoveryListener> allDiscoveryListeners = new ArrayList<>(discoveryListeners.size() + 1);
allDiscoveryListeners.addAll(discoveryListeners);
allDiscoveryListeners.add(defaultDiscoveryListener);
return LauncherDiscoveryListeners.composite(allDiscoveryListeners);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.TestExecutionListener;

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

Set<TestEngine> engines = collectTestEngines(config);
List<PostDiscoveryFilter> filters = collectPostDiscoveryFilters(config);
List<LauncherDiscoveryListener> discoveryListeners = collectLauncherDiscoveryListeners(config);

Launcher launcher = new DefaultLauncher(engines, filters, discoveryListeners);

registerTestExecutionListeners(config, launcher);

return launcher;
}

private static Set<TestEngine> collectTestEngines(LauncherConfig config) {
Set<TestEngine> engines = new LinkedHashSet<>();
if (config.isTestEngineAutoRegistrationEnabled()) {
new ServiceLoaderTestEngineRegistry().loadTestEngines().forEach(engines::add);
}
engines.addAll(config.getAdditionalTestEngines());
return engines;
}

private static List<PostDiscoveryFilter> collectPostDiscoveryFilters(LauncherConfig config) {
List<PostDiscoveryFilter> filters = new ArrayList<>();
if (config.isPostDiscoveryFilterAutoRegistrationEnabled()) {
new ServiceLoaderPostDiscoveryFilterRegistry().loadPostDiscoveryFilters().forEach(filters::add);
}
filters.addAll(config.getAdditionalPostDiscoveryFilters());
return filters;
}

Launcher launcher = new DefaultLauncher(engines, filters);
private static List<LauncherDiscoveryListener> collectLauncherDiscoveryListeners(LauncherConfig config) {
List<LauncherDiscoveryListener> discoveryListeners = new ArrayList<>();
if (config.isLauncherDiscoveryListenerAutoRegistrationEnabled()) {
new ServiceLoaderLauncherDiscoveryListenerRegistry().loadListeners().forEach(discoveryListeners::add);
}
return discoveryListeners;
}

private static void registerTestExecutionListeners(LauncherConfig config, Launcher launcher) {
if (config.isTestExecutionListenerAutoRegistrationEnabled()) {
loadAndFilterTestExecutionListeners().forEach(launcher::registerTestExecutionListeners);
}
config.getAdditionalTestExecutionListeners().forEach(launcher::registerTestExecutionListeners);

return launcher;
}

private static Stream<TestExecutionListener> loadAndFilterTestExecutionListeners() {
Expand Down
Loading

0 comments on commit 8b71ee6

Please sign in to comment.