Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce LauncherInterceptor API #3091

Merged
merged 7 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ endif::[]
:LauncherDiscoveryRequest: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherDiscoveryRequest.html[LauncherDiscoveryRequest]
:LauncherDiscoveryRequestBuilder: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.html[LauncherDiscoveryRequestBuilder]
:LauncherFactory: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/core/LauncherFactory.html[LauncherFactory]
:LauncherInterceptor: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherInterceptor.html[LauncherInterceptor]
:LauncherSession: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherSession.html[LauncherSession]
:LauncherSessionListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherSessionListener.html[LauncherSessionListener]
:LoggingListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/listeners/LoggingListener.html[LoggingListener]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ repository on GitHub.

==== Deprecations and Breaking Changes

* ❓
* Building native images with GraalVM now requires configuring the build arg
`--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig`.

==== New Features and Improvements

Expand All @@ -30,6 +31,11 @@ repository on GitHub.
methods are called in reverse order compared to the former when multiple listeners are
registered. This affects the following listener interfaces: `TestExecutionListener`,
`EngineExecutionListener`, `LauncherDiscoveryListener`, and `LauncherSessionListener`.
* Introduce `LauncherInterceptor` SPI for intercepting the creation of instances of
`Launcher` and `LauncherSessionlistener` as well as calls for `discover` and `execute`
of the former. Please refer to the
<<../user-guide/index.adoc#launcher-api-launcher-interceptors-custom, User Guide>> for
details.

[[release-notes-5.10.0-M1-junit-jupiter]]
=== JUnit Jupiter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,24 @@ include::{testDir}/example/session/HttpTests.java[tags=user_guide]
<3> Send a request to the server
<4> Check the status code of the response

[[launcher-api-launcher-interceptors-custom]]
==== Registering a LauncherInterceptor

In order to intercept the creation of instances of `{Launcher}` and
`{LauncherSessionListener}` and calls to the `discover` and `execute` methods of the
former, clients can registercustom implementations of `{LauncherInterceptor}` via Java's
`{ServiceLoader}` mechanism by additionally setting the
`junit.platform.launcher.interceptors.enabled` <<running-tests-config-params,
configuration parameter>> to `true`.

A typical use case is to create a custom replace the `ClassLoader` used by the JUnit
Platform to load test classes and engine implementations.

[source,java]
----
include::{testDir}/example/CustomLauncherInterceptor.java[tags=user_guide]
----

[[launcher-api-launcher-discovery-listeners-custom]]
==== Registering a LauncherDiscoveryListener

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -979,14 +979,16 @@ because particularly when
to attribute it to a specific test or container.

[[running-tests-listeners]]
=== Using Listeners
=== Using Listeners and Interceptors

The JUnit Platform provides the following listener APIs that allow JUnit, third parties,
and custom user code to react to events fired at various points during the discovery and
execution of a `TestPlan`.

* `{LauncherSessionListener}`: receives events when a `{LauncherSession}` is opened and
closed.
* `{LauncherInterceptor}`: intercepts test discovery and execution in the context of a
`LauncherSession`.
* `{LauncherDiscoveryListener}`: receives events that occur during test discovery.
* `{TestExecutionListener}`: receives events that occur during test execution.

Expand All @@ -1003,6 +1005,7 @@ For details on registering and configuring listeners, see the following sections
guide.

* <<launcher-api-launcher-session-listeners-custom>>
* <<launcher-api-launcher-interceptors-custom>>
* <<launcher-api-launcher-discovery-listeners-custom>>
* <<launcher-api-listeners-custom>>
* <<launcher-api-listeners-config>>
Expand Down
55 changes: 55 additions & 0 deletions documentation/src/test/java/example/CustomLauncherInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

// tag::user_guide[]

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;

import org.junit.platform.launcher.LauncherInterceptor;

public class CustomLauncherInterceptor implements LauncherInterceptor {

private final URLClassLoader customClassLoader;

public CustomLauncherInterceptor() throws Exception {
ClassLoader parent = Thread.currentThread().getContextClassLoader();
customClassLoader = new URLClassLoader(new URL[] { URI.create("some.jar").toURL() }, parent);
}

@Override
public <T> T intercept(Invocation<T> invocation) {
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(customClassLoader);
try {
return invocation.proceed();
}
finally {
currentThread.setContextClassLoader(originalClassLoader);
}
}

@Override
public void close() {
try {
customClassLoader.close();
}
catch (IOException e) {
throw new UncheckedIOException("Failed to close custom class loader", e);
}
}
}
// end::user_guide[]
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,27 @@
*/
public class TestEngineSpy implements TestEngine {

public static final String ID = TestEngineSpy.class.getSimpleName();
private final String id;

public ExecutionRequest requestForExecution;

public TestEngineSpy() {
this(TestEngineSpy.class.getSimpleName());
}

public TestEngineSpy(String id) {
this.id = id;
}

@Override
public String getId() {
return ID;
return id;
}

@Override
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
var engineUniqueId = UniqueId.forEngine(ID);
var engineDescriptor = new TestDescriptorStub(engineUniqueId, ID);
var engineUniqueId = UniqueId.forEngine(id);
var engineDescriptor = new TestDescriptorStub(engineUniqueId, id);
var testDescriptor = new TestDescriptorStub(engineUniqueId.append("test", "test"), "test");
engineDescriptor.addChild(testDescriptor);
return engineDescriptor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.platform.launcher;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

import org.apiguardian.api.API;
Expand Down Expand Up @@ -135,6 +136,18 @@ public class LauncherConstants {
*/
public static final String DEACTIVATE_ALL_LISTENERS_PATTERN = ClassNamePatternFilterUtils.DEACTIVATE_ALL_PATTERN;

/**
* Property name used to enable support for
* {@link LauncherInterceptor} instances to be registered via the
* {@link java.util.ServiceLoader ServiceLoader} mechanism: {@value}
*
* <p>By default, interceptor registration is disabled.
*
* @see LauncherInterceptor
*/
@API(status = EXPERIMENTAL, since = "1.10")
public static final String ENABLE_LAUNCHER_INTERCEPTORS = "junit.platform.launcher.interceptors.enabled";

private LauncherConstants() {
/* no-op */
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.launcher;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import org.apiguardian.api.API;

/**
* Interceptor for test discovery and execution by a {@link Launcher} in the
* context of a {@link LauncherSession}.
*
* <p>Interceptors are instantiated once per {@link LauncherSession} and closed
* after the session is closed. They can
* {@linkplain #intercept(Invocation) intercept} the following invocations:
* <ul>
* <li>
* creation of {@link LauncherSessionListener} instances registered via the
* {@link java.util.ServiceLoader ServiceLoader} mechanism
* </li>
* <li>
* creation of {@link Launcher} instances
* </li>
* <li>
* calls to {@link Launcher#discover(LauncherDiscoveryRequest)},
* {@link Launcher#execute(TestPlan, TestExecutionListener...)}, and
* {@link Launcher#execute(LauncherDiscoveryRequest, TestExecutionListener...)}
* </li>
* </ul>
*
* <p>Implementations of this interface can be registered via the
* {@link java.util.ServiceLoader ServiceLoader} mechanism by additionally
* setting the {@value LauncherConstants#ENABLE_LAUNCHER_INTERCEPTORS}
* configuration parameter to {@code true}.
*
* <p>A typical use case is to create a custom {@link ClassLoader} in the
* constructor of the implementing class, replace the
* {@link Thread#setContextClassLoader(ClassLoader) contextClassLoader} of the
* current thread while {@link #intercept(Invocation) intercepting} invocations,
* and close the custom {@code ClassLoader} in {@link #close()}
*
* @since 1.10
* @see Launcher
* @see LauncherSession
* @see LauncherConstants#ENABLE_LAUNCHER_INTERCEPTORS
*/
@API(status = EXPERIMENTAL, since = "1.10")
public interface LauncherInterceptor {

/**
* Intercept the supplied invocation.
*
* <p>Implementations must call {@link Invocation#proceed()} exactly once.
*
* @param invocation the intercepted invocation; never {@code null}
* @return the result of the invocation
*/
<T> T intercept(Invocation<T> invocation);

/**
* Closes this interceptor.
*
* <p>Any resources held by this interceptor should be released by this
* method.
*/
void close();

/**
* An invocation that can be intercepted.
*
* <p>This interface is not intended to be implemented by clients.
*/
interface Invocation<T> {
T proceed();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@
* @see Launcher
* @see LauncherFactory
*/
class DefaultLauncher implements InternalLauncher {
class DefaultLauncher implements Launcher {

private final ListenerRegistry<LauncherDiscoveryListener> launcherDiscoveryListenerRegistry = ListenerRegistry.forLauncherDiscoveryListeners();
private final ListenerRegistry<TestExecutionListener> testExecutionListenerRegistry = ListenerRegistry.forTestExecutionListeners();
private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry();
private final EngineExecutionOrchestrator executionOrchestrator = new EngineExecutionOrchestrator(
testExecutionListenerRegistry);
listenerRegistry.testExecutionListeners);;
private final EngineDiscoveryOrchestrator discoveryOrchestrator;

/**
Expand All @@ -59,17 +58,17 @@ class DefaultLauncher implements InternalLauncher {
Preconditions.containsNoNullElements(postDiscoveryFilters,
"PostDiscoveryFilter array must not contain null elements");
this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(testEngines,
unmodifiableCollection(postDiscoveryFilters), launcherDiscoveryListenerRegistry);
unmodifiableCollection(postDiscoveryFilters), listenerRegistry.launcherDiscoveryListeners);
}

@Override
public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) {
this.launcherDiscoveryListenerRegistry.addAll(listeners);
this.listenerRegistry.launcherDiscoveryListeners.addAll(listeners);
}

@Override
public void registerTestExecutionListeners(TestExecutionListener... listeners) {
this.testExecutionListenerRegistry.addAll(listeners);
this.listenerRegistry.testExecutionListeners.addAll(listeners);
}

@Override
Expand All @@ -95,16 +94,6 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
execute((InternalTestPlan) testPlan, listeners);
}

@Override
public ListenerRegistry<TestExecutionListener> getTestExecutionListenerRegistry() {
return testExecutionListenerRegistry;
}

@Override
public ListenerRegistry<LauncherDiscoveryListener> getLauncherDiscoveryListenerRegistry() {
return launcherDiscoveryListenerRegistry;
}

private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest,
EngineDiscoveryOrchestrator.Phase phase) {
return discoveryOrchestrator.discover(discoveryRequest, phase);
Expand Down
Loading