diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 025ab6fc86d4..224d99ea4bdd 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -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] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc index daea2f4ed0f6..15f400b83b4e 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc @@ -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 @@ -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 diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc index 4fcd0996c9df..8f98a7e825b1 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc @@ -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` <> 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 diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index e88d0056e5fe..cdff368b4f3a 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -979,7 +979,7 @@ 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 @@ -987,6 +987,8 @@ 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. @@ -1003,6 +1005,7 @@ For details on registering and configuring listeners, see the following sections guide. * <> +* <> * <> * <> * <> diff --git a/documentation/src/test/java/example/CustomLauncherInterceptor.java b/documentation/src/test/java/example/CustomLauncherInterceptor.java new file mode 100644 index 000000000000..149cf7e45440 --- /dev/null +++ b/documentation/src/test/java/example/CustomLauncherInterceptor.java @@ -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 intercept(Invocation 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[] diff --git a/junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/TestEngineSpy.java b/junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/TestEngineSpy.java index 2cb30350c351..cdf6c136345a 100644 --- a/junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/TestEngineSpy.java +++ b/junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/TestEngineSpy.java @@ -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; diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java index b4f5916f00c1..12f52e2edf05 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java @@ -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; @@ -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} + * + *

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 */ } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherInterceptor.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherInterceptor.java new file mode 100644 index 000000000000..262a1d539809 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherInterceptor.java @@ -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}. + * + *

Interceptors are instantiated once per {@link LauncherSession} and closed + * after the session is closed. They can + * {@linkplain #intercept(Invocation) intercept} the following invocations: + *

    + *
  • + * creation of {@link LauncherSessionListener} instances registered via the + * {@link java.util.ServiceLoader ServiceLoader} mechanism + *
  • + *
  • + * creation of {@link Launcher} instances + *
  • + *
  • + * calls to {@link Launcher#discover(LauncherDiscoveryRequest)}, + * {@link Launcher#execute(TestPlan, TestExecutionListener...)}, and + * {@link Launcher#execute(LauncherDiscoveryRequest, TestExecutionListener...)} + *
  • + *
+ * + *

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}. + * + *

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. + * + *

Implementations must call {@link Invocation#proceed()} exactly once. + * + * @param invocation the intercepted invocation; never {@code null} + * @return the result of the invocation + */ + T intercept(Invocation invocation); + + /** + * Closes this interceptor. + * + *

Any resources held by this interceptor should be released by this + * method. + */ + void close(); + + /** + * An invocation that can be intercepted. + * + *

This interface is not intended to be implemented by clients. + */ + interface Invocation { + T proceed(); + } + +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java index 3b0dc3e03f91..9f21595d0d64 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java @@ -35,12 +35,11 @@ * @see Launcher * @see LauncherFactory */ -class DefaultLauncher implements InternalLauncher { +class DefaultLauncher implements Launcher { - private final ListenerRegistry launcherDiscoveryListenerRegistry = ListenerRegistry.forLauncherDiscoveryListeners(); - private final ListenerRegistry testExecutionListenerRegistry = ListenerRegistry.forTestExecutionListeners(); + private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry(); private final EngineExecutionOrchestrator executionOrchestrator = new EngineExecutionOrchestrator( - testExecutionListenerRegistry); + listenerRegistry.testExecutionListeners);; private final EngineDiscoveryOrchestrator discoveryOrchestrator; /** @@ -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 @@ -95,16 +94,6 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { execute((InternalTestPlan) testPlan, listeners); } - @Override - public ListenerRegistry getTestExecutionListenerRegistry() { - return testExecutionListenerRegistry; - } - - @Override - public ListenerRegistry getLauncherDiscoveryListenerRegistry() { - return launcherDiscoveryListenerRegistry; - } - private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, EngineDiscoveryOrchestrator.Phase phase) { return discoveryOrchestrator.discover(discoveryRequest, phase); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java index 79f6d815fc76..b3743ac253a7 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java @@ -10,10 +10,14 @@ package org.junit.platform.launcher.core; +import java.util.List; +import java.util.function.Supplier; + import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.LauncherInterceptor; import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TestExecutionListener; @@ -24,12 +28,35 @@ */ class DefaultLauncherSession implements LauncherSession { - private final DelegatingLauncher launcher; + private static final LauncherInterceptor NOOP_INTERCEPTOR = new LauncherInterceptor() { + @Override + public T intercept(Invocation invocation) { + return invocation.proceed(); + } + + @Override + public void close() { + // do nothing + } + }; + + private final LauncherInterceptor interceptor; private final LauncherSessionListener listener; + private final DelegatingLauncher launcher; - DefaultLauncherSession(Launcher launcher, LauncherSessionListener listener) { + DefaultLauncherSession(List interceptors, Supplier listenerSupplier, + Supplier launcherSupplier) { + interceptor = composite(interceptors); + Launcher launcher; + if (interceptor == NOOP_INTERCEPTOR) { + this.listener = listenerSupplier.get(); + launcher = launcherSupplier.get(); + } + else { + this.listener = interceptor.intercept(listenerSupplier::get); + launcher = new InterceptingLauncher(interceptor.intercept(launcherSupplier::get), interceptor); + } this.launcher = new DelegatingLauncher(launcher); - this.listener = listener; listener.launcherSessionOpened(this); } @@ -44,51 +71,10 @@ LauncherSessionListener getListener() { @Override public void close() { - if (launcher.getDelegate() != ClosedLauncher.INSTANCE) { - launcher.setDelegate(ClosedLauncher.INSTANCE); + if (launcher.delegate != ClosedLauncher.INSTANCE) { + launcher.delegate = ClosedLauncher.INSTANCE; listener.launcherSessionClosed(this); - } - } - - private static class DelegatingLauncher implements Launcher { - - private Launcher delegate; - - DelegatingLauncher(Launcher delegate) { - this.delegate = delegate; - } - - public Launcher getDelegate() { - return delegate; - } - - public void setDelegate(Launcher delegate) { - this.delegate = delegate; - } - - @Override - public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) { - delegate.registerLauncherDiscoveryListeners(listeners); - } - - @Override - public void registerTestExecutionListeners(TestExecutionListener... listeners) { - delegate.registerTestExecutionListeners(listeners); - } - - @Override - public TestPlan discover(LauncherDiscoveryRequest launcherDiscoveryRequest) { - return delegate.discover(launcherDiscoveryRequest); - } - - @Override - public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners) { - delegate.execute(launcherDiscoveryRequest, listeners); - } - - @Override - public void execute(TestPlan testPlan, TestExecutionListener... listeners) { - delegate.execute(testPlan, listeners); + interceptor.close(); } } @@ -124,4 +110,28 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { throw new PreconditionViolationException("Launcher session has already been closed"); } } + + private static LauncherInterceptor composite(List interceptors) { + if (interceptors.isEmpty()) { + return NOOP_INTERCEPTOR; + } + return interceptors.stream() // + .skip(1) // + .reduce(interceptors.get(0), (a, b) -> new LauncherInterceptor() { + @Override + public void close() { + try { + a.close(); + } + finally { + b.close(); + } + } + + @Override + public T intercept(Invocation invocation) { + return a.intercept(() -> b.intercept(invocation)); + } + }); + } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java new file mode 100644 index 000000000000..d4332b52fe35 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java @@ -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 org.junit.platform.launcher.core; + +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; + +/** + * @since 1.10 + */ +class DelegatingLauncher implements Launcher { + + protected Launcher delegate; + + DelegatingLauncher(Launcher delegate) { + this.delegate = delegate; + } + + @Override + public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) { + delegate.registerLauncherDiscoveryListeners(listeners); + } + + @Override + public void registerTestExecutionListeners(TestExecutionListener... listeners) { + delegate.registerTestExecutionListeners(listeners); + } + + @Override + public TestPlan discover(LauncherDiscoveryRequest launcherDiscoveryRequest) { + return delegate.discover(launcherDiscoveryRequest); + } + + @Override + public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners) { + delegate.execute(launcherDiscoveryRequest, listeners); + } + + @Override + public void execute(TestPlan testPlan, TestExecutionListener... listeners) { + delegate.execute(testPlan, listeners); + } + +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineIdValidator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineIdValidator.java index adf3739b39e0..abeeb298acaa 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineIdValidator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineIdValidator.java @@ -23,8 +23,6 @@ */ class EngineIdValidator { - private static final Logger logger = LoggerFactory.getLogger(EngineIdValidator.class); - private EngineIdValidator() { } @@ -33,7 +31,7 @@ static Iterable validate(Iterable testEngines) { for (TestEngine testEngine : testEngines) { // check usage of reserved id prefix if (!validateReservedIds(testEngine)) { - logger.warn(() -> String.format( + getLogger().warn(() -> String.format( "Third-party TestEngine implementations are forbidden to use the reserved 'junit-' prefix for their ID: '%s'", testEngine.getId())); } @@ -47,6 +45,11 @@ static Iterable validate(Iterable testEngines) { return testEngines; } + private static Logger getLogger() { + // Not a constant to avoid problems with building GraalVM native images + return LoggerFactory.getLogger(EngineIdValidator.class); + } + // https://github.com/junit-team/junit5/issues/1557 private static boolean validateReservedIds(TestEngine testEngine) { String engineId = testEngine.getId(); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingLauncher.java new file mode 100644 index 000000000000..043d4e9688f5 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingLauncher.java @@ -0,0 +1,51 @@ +/* + * 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.core; + +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.LauncherInterceptor; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; + +/** + * @since 1.10 + */ +class InterceptingLauncher extends DelegatingLauncher { + + private final LauncherInterceptor interceptor; + + InterceptingLauncher(Launcher delegate, LauncherInterceptor interceptor) { + super(delegate); + this.interceptor = interceptor; + } + + @Override + public TestPlan discover(LauncherDiscoveryRequest launcherDiscoveryRequest) { + return interceptor.intercept(() -> super.discover(launcherDiscoveryRequest)); + } + + @Override + public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners) { + interceptor.intercept(() -> { + super.execute(launcherDiscoveryRequest, listeners); + return null; + }); + } + + @Override + public void execute(TestPlan testPlan, TestExecutionListener... listeners) { + interceptor.intercept(() -> { + super.execute(testPlan, listeners); + return null; + }); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java index 3ac0b4c8330d..172db2b89a34 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java @@ -10,9 +10,11 @@ package org.junit.platform.launcher.core; +import static java.util.Collections.emptyList; import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import static org.junit.platform.launcher.LauncherConstants.DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.ENABLE_LAUNCHER_INTERCEPTORS; import java.util.ArrayList; import java.util.LinkedHashSet; @@ -29,6 +31,7 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.launcher.LauncherInterceptor; import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.PostDiscoveryFilter; @@ -63,8 +66,6 @@ @API(status = STABLE, since = "1.0") public class LauncherFactory { - private static final ServiceLoaderRegistry SERVICE_LOADER_REGISTRY = new ServiceLoaderRegistry(); - private LauncherFactory() { /* no-op */ } @@ -95,7 +96,10 @@ public static LauncherSession openSession() throws PreconditionViolationExceptio */ @API(status = EXPERIMENTAL, since = "1.8") public static LauncherSession openSession(LauncherConfig config) throws PreconditionViolationException { - return new DefaultLauncherSession(createDefaultLauncher(config), createLauncherSessionListener(config)); + Preconditions.notNull(config, "LauncherConfig must not be null"); + LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); + return new DefaultLauncherSession(collectLauncherInterceptors(configurationParameters), + () -> createLauncherSessionListener(config), () -> createDefaultLauncher(config, configurationParameters)); } /** @@ -122,23 +126,35 @@ public static Launcher create() throws PreconditionViolationException { */ @API(status = EXPERIMENTAL, since = "1.3") public static Launcher create(LauncherConfig config) throws PreconditionViolationException { - return new SessionPerRequestLauncher(createDefaultLauncher(config), createLauncherSessionListener(config)); - } - - private static DefaultLauncher createDefaultLauncher(LauncherConfig config) { Preconditions.notNull(config, "LauncherConfig must not be null"); + LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); + return new SessionPerRequestLauncher(() -> createDefaultLauncher(config, configurationParameters), + () -> createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); + } + private static DefaultLauncher createDefaultLauncher(LauncherConfig config, + LauncherConfigurationParameters configurationParameters) { Set engines = collectTestEngines(config); List filters = collectPostDiscoveryFilters(config); DefaultLauncher launcher = new DefaultLauncher(engines, filters); registerLauncherDiscoveryListeners(config, launcher); - registerTestExecutionListeners(config, launcher); + registerTestExecutionListeners(config, launcher, configurationParameters); return launcher; } + private static List collectLauncherInterceptors( + LauncherConfigurationParameters configurationParameters) { + if (configurationParameters.getBoolean(ENABLE_LAUNCHER_INTERCEPTORS).orElse(false)) { + List interceptors = new ArrayList<>(); + ServiceLoaderRegistry.load(LauncherInterceptor.class).forEach(interceptors::add); + return interceptors; + } + return emptyList(); + } + private static Set collectTestEngines(LauncherConfig config) { Set engines = new LinkedHashSet<>(); if (config.isTestEngineAutoRegistrationEnabled()) { @@ -151,7 +167,7 @@ private static Set collectTestEngines(LauncherConfig config) { private static LauncherSessionListener createLauncherSessionListener(LauncherConfig config) { ListenerRegistry listenerRegistry = ListenerRegistry.forLauncherSessionListeners(); if (config.isLauncherSessionListenerAutoRegistrationEnabled()) { - SERVICE_LOADER_REGISTRY.load(LauncherSessionListener.class).forEach(listenerRegistry::add); + ServiceLoaderRegistry.load(LauncherSessionListener.class).forEach(listenerRegistry::add); } config.getAdditionalLauncherSessionListeners().forEach(listenerRegistry::add); return listenerRegistry.getCompositeListener(); @@ -160,7 +176,7 @@ private static LauncherSessionListener createLauncherSessionListener(LauncherCon private static List collectPostDiscoveryFilters(LauncherConfig config) { List filters = new ArrayList<>(); if (config.isPostDiscoveryFilterAutoRegistrationEnabled()) { - SERVICE_LOADER_REGISTRY.load(PostDiscoveryFilter.class).forEach(filters::add); + ServiceLoaderRegistry.load(PostDiscoveryFilter.class).forEach(filters::add); } filters.addAll(config.getAdditionalPostDiscoveryFilters()); return filters; @@ -168,22 +184,24 @@ private static List collectPostDiscoveryFilters(LauncherCon private static void registerLauncherDiscoveryListeners(LauncherConfig config, Launcher launcher) { if (config.isLauncherDiscoveryListenerAutoRegistrationEnabled()) { - SERVICE_LOADER_REGISTRY.load(LauncherDiscoveryListener.class).forEach( + ServiceLoaderRegistry.load(LauncherDiscoveryListener.class).forEach( launcher::registerLauncherDiscoveryListeners); } config.getAdditionalLauncherDiscoveryListeners().forEach(launcher::registerLauncherDiscoveryListeners); } - private static void registerTestExecutionListeners(LauncherConfig config, Launcher launcher) { + private static void registerTestExecutionListeners(LauncherConfig config, Launcher launcher, + LauncherConfigurationParameters configurationParameters) { if (config.isTestExecutionListenerAutoRegistrationEnabled()) { - loadAndFilterTestExecutionListeners().forEach(launcher::registerTestExecutionListeners); + loadAndFilterTestExecutionListeners(configurationParameters).forEach( + launcher::registerTestExecutionListeners); } config.getAdditionalTestExecutionListeners().forEach(launcher::registerTestExecutionListeners); } - private static Stream loadAndFilterTestExecutionListeners() { - Iterable listeners = SERVICE_LOADER_REGISTRY.load(TestExecutionListener.class); - ConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); + private static Stream loadAndFilterTestExecutionListeners( + ConfigurationParameters configurationParameters) { + Iterable listeners = ServiceLoaderRegistry.load(TestExecutionListener.class); String deactivatedListenersPattern = configurationParameters.get( DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME).orElse(null); // @formatter:off diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherListenerRegistry.java similarity index 63% rename from junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalLauncher.java rename to junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherListenerRegistry.java index 453be6931729..17b21b141f07 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherListenerRegistry.java @@ -10,16 +10,10 @@ package org.junit.platform.launcher.core; -import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.TestExecutionListener; -/** - * @since 1.8 - */ -interface InternalLauncher extends Launcher { - - ListenerRegistry getTestExecutionListenerRegistry(); - - ListenerRegistry getLauncherDiscoveryListenerRegistry(); +class LauncherListenerRegistry { + final ListenerRegistry launcherDiscoveryListeners = ListenerRegistry.forLauncherDiscoveryListeners(); + final ListenerRegistry testExecutionListeners = ListenerRegistry.forTestExecutionListeners(); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java index 47ccd2f23095..54b446cce41a 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java @@ -24,13 +24,16 @@ */ class ServiceLoaderRegistry { - private static final Logger logger = LoggerFactory.getLogger(ServiceLoaderRegistry.class); - - Iterable load(Class serviceProviderClass) { + static Iterable load(Class serviceProviderClass) { Iterable listeners = ServiceLoader.load(serviceProviderClass, ClassLoaderUtils.getDefaultClassLoader()); - logger.config(() -> "Loaded " + serviceProviderClass.getSimpleName() + " instances: " + getLogger().config(() -> "Loaded " + serviceProviderClass.getSimpleName() + " instances: " + stream(listeners.spliterator(), false).map(Object::toString).collect(toList())); return listeners; } + private static Logger getLogger() { + // Not a constant to avoid problems with building GraalVM native images + return LoggerFactory.getLogger(ServiceLoaderRegistry.class); + } + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderTestEngineRegistry.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderTestEngineRegistry.java index 00233a6b44db..9959e6e3a2e7 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderTestEngineRegistry.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderTestEngineRegistry.java @@ -29,13 +29,16 @@ public final class ServiceLoaderTestEngineRegistry { public ServiceLoaderTestEngineRegistry() { } - private static final Logger logger = LoggerFactory.getLogger(ServiceLoaderTestEngineRegistry.class); - public Iterable loadTestEngines() { Iterable testEngines = ServiceLoader.load(TestEngine.class, ClassLoaderUtils.getDefaultClassLoader()); - logger.config(() -> TestEngineFormatter.format("Discovered TestEngines", testEngines)); + getLogger().config(() -> TestEngineFormatter.format("Discovered TestEngines", testEngines)); return testEngines; } + private static Logger getLogger() { + // Not a constant to avoid problems with building GraalVM native images + return LoggerFactory.getLogger(ServiceLoaderTestEngineRegistry.class); + } + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java index 9c0b7dd47e60..167efb6443ec 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java @@ -10,8 +10,13 @@ package org.junit.platform.launcher.core; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.LauncherInterceptor; import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TestExecutionListener; @@ -20,24 +25,29 @@ /** * @since 1.8 */ -class SessionPerRequestLauncher implements InternalLauncher { +class SessionPerRequestLauncher implements Launcher { - private final InternalLauncher delegate; - private final LauncherSessionListener sessionListener; + private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry(); + private final Supplier launcherSupplier; + private final Supplier sessionListenerSupplier; + private final Supplier> interceptorFactory; - SessionPerRequestLauncher(InternalLauncher delegate, LauncherSessionListener sessionListener) { - this.delegate = delegate; - this.sessionListener = sessionListener; + SessionPerRequestLauncher(Supplier launcherSupplier, + Supplier sessionListenerSupplier, + Supplier> interceptorFactory) { + this.launcherSupplier = launcherSupplier; + this.sessionListenerSupplier = sessionListenerSupplier; + this.interceptorFactory = interceptorFactory; } @Override public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) { - delegate.registerLauncherDiscoveryListeners(listeners); + listenerRegistry.launcherDiscoveryListeners.addAll(listeners); } @Override public void registerTestExecutionListeners(TestExecutionListener... listeners) { - delegate.registerTestExecutionListeners(listeners); + listenerRegistry.testExecutionListeners.addAll(listeners); } @Override @@ -61,17 +71,13 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { } } - @Override - public ListenerRegistry getTestExecutionListenerRegistry() { - return delegate.getTestExecutionListenerRegistry(); - } - - @Override - public ListenerRegistry getLauncherDiscoveryListenerRegistry() { - return delegate.getLauncherDiscoveryListenerRegistry(); - } - private LauncherSession createSession() { - return new DefaultLauncherSession(delegate, sessionListener); + LauncherSession session = new DefaultLauncherSession(interceptorFactory.get(), sessionListenerSupplier, + launcherSupplier); + Launcher launcher = session.getLauncher(); + listenerRegistry.launcherDiscoveryListeners.getListeners().forEach( + launcher::registerLauncherDiscoveryListeners); + listenerRegistry.testExecutionListeners.getListeners().forEach(launcher::registerTestExecutionListeners); + return session; } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/InterceptedTestEngine.java b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptedTestEngine.java new file mode 100644 index 000000000000..dc57b1ff7a22 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptedTestEngine.java @@ -0,0 +1,22 @@ +/* + * 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 org.junit.platform.fakes.TestEngineSpy; + +public class InterceptedTestEngine extends TestEngineSpy { + + public static final String ID = "intercepted-engine"; + + public InterceptedTestEngine() { + super(ID); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java new file mode 100644 index 000000000000..bbaed89e2e91 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java @@ -0,0 +1,36 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class InterceptorInjectedLauncherSessionListener implements LauncherSessionListener { + + public static int CALLS; + + public InterceptorInjectedLauncherSessionListener() { + assertEquals(TestLauncherInterceptor1.CLASSLOADER_NAME, + Thread.currentThread().getContextClassLoader().getName()); + assertTrue(TestLauncherInterceptor2.INTERCEPTING); + } + + @Override + public void launcherSessionOpened(LauncherSession session) { + CALLS++; + } + + @Override + public void launcherSessionClosed(LauncherSession session) { + assertEquals(TestLauncherInterceptor1.CLASSLOADER_NAME, + Thread.currentThread().getContextClassLoader().getName()); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherDiscoveryListener.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherDiscoveryListener.java index 489f63caaeb3..7a5be0c967a4 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherDiscoveryListener.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherDiscoveryListener.java @@ -11,20 +11,10 @@ package org.junit.platform.launcher; public class TestLauncherDiscoveryListener implements LauncherDiscoveryListener { + public static boolean called; @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - return getClass() == obj.getClass(); - } - - @Override - public int hashCode() { - return 1; + public void launcherDiscoveryStarted(LauncherDiscoveryRequest request) { + called = true; } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java new file mode 100644 index 000000000000..6ff0dc7046dd --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java @@ -0,0 +1,49 @@ +/* + * 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 java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLClassLoader; + +public class TestLauncherInterceptor1 implements LauncherInterceptor { + + public static final String CLASSLOADER_NAME = "interceptor-loader"; + + private final ClassLoader originalClassLoader; + private final URLClassLoader replacedClassLoader; + + public TestLauncherInterceptor1() { + originalClassLoader = Thread.currentThread().getContextClassLoader(); + var url = getClass().getClassLoader().getResource("intercepted-testservices/"); + replacedClassLoader = new URLClassLoader(CLASSLOADER_NAME, new URL[] { url }, originalClassLoader); + Thread.currentThread().setContextClassLoader(replacedClassLoader); + } + + @Override + public T intercept(Invocation invocation) { + return invocation.proceed(); + } + + @Override + public void close() { + try { + replacedClassLoader.close(); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java new file mode 100644 index 000000000000..04f9ad3e29a0 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java @@ -0,0 +1,31 @@ +/* + * 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; + +public class TestLauncherInterceptor2 implements LauncherInterceptor { + + public static boolean INTERCEPTING; + + @Override + public T intercept(Invocation invocation) { + INTERCEPTING = true; + try { + return invocation.proceed(); + } + finally { + INTERCEPTING = false; + } + } + + @Override + public void close() { + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java index dce04470cd5c..e0e7a3c4bf57 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java @@ -76,17 +76,20 @@ class DefaultLauncherTests { @Test void constructLauncherWithoutAnyEngines() { + var launcher = createLauncher(); + Throwable exception = assertThrows(PreconditionViolationException.class, - LauncherFactoryForTestingPurposesOnly::createLauncher); + () -> launcher.discover(request().build())); assertThat(exception).hasMessageContaining("Cannot create Launcher without at least one TestEngine"); } @Test void constructLauncherWithMultipleTestEnginesWithDuplicateIds() { - var exception = assertThrows(JUnitException.class, - () -> createLauncher(new DemoHierarchicalTestEngine("dummy id"), - new DemoHierarchicalTestEngine("dummy id"))); + var launcher = createLauncher(new DemoHierarchicalTestEngine("dummy id"), + new DemoHierarchicalTestEngine("dummy id")); + + var exception = assertThrows(JUnitException.class, () -> launcher.discover(request().build())); assertThat(exception).hasMessageContaining("multiple engines with the same ID"); } @@ -486,11 +489,11 @@ public Type getType() { @Test void reportsDynamicTestDescriptorsCorrectly() { - var engineId = UniqueId.forEngine(TestEngineSpy.ID); + var engineId = UniqueId.forEngine("engine"); var containerAndTestId = engineId.append("c&t", "c&t"); var dynamicTestId = containerAndTestId.append("test", "test"); - var engine = new TestEngineSpy() { + var engine = new TestEngineSpy(engineId.getLastSegment().getValue()) { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { @@ -595,7 +598,8 @@ void testPlanThrowsExceptionWhenModified() { @TrackLogRecords void thirdPartyEngineUsingReservedEngineIdPrefixEmitsWarning(LogRecordListener listener) { var id = "junit-using-reserved-prefix"; - createLauncher(new TestEngineStub(id)); + var launcher = createLauncher(new TestEngineStub(id)); + launcher.discover(request().build()); assertThat(listener.stream(EngineIdValidator.class, Level.WARNING).map(LogRecord::getMessage)) // .containsExactly( "Third-party TestEngine implementations are forbidden to use the reserved 'junit-' prefix for their ID: '" @@ -614,7 +618,8 @@ void thirdPartyEngineClaimingToBeVintageResultsInException() { private void assertImposter(String id) { TestEngine impostor = new TestEngineStub(id); - Exception exception = assertThrows(JUnitException.class, () -> createLauncher(impostor)); + var launcher = createLauncher(impostor); + Exception exception = assertThrows(JUnitException.class, () -> launcher.discover(request().build())); assertThat(exception).hasMessage( "Third-party TestEngine '%s' is forbidden to use the reserved '%s' TestEngine ID.", impostor.getClass().getName(), id); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java index d33d88956201..e769149edfff 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java @@ -12,24 +12,40 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.LauncherConstants.DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.ENABLE_LAUNCHER_INTERCEPTORS; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URL; import java.net.URLClassLoader; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; -import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.fakes.TestEngineSpy; +import org.junit.platform.launcher.InterceptedTestEngine; +import org.junit.platform.launcher.InterceptorInjectedLauncherSessionListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TagFilter; +import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestLauncherDiscoveryListener; +import org.junit.platform.launcher.TestLauncherInterceptor1; +import org.junit.platform.launcher.TestLauncherInterceptor2; import org.junit.platform.launcher.TestLauncherSessionListener; import org.junit.platform.launcher.listeners.AnotherUnusedTestExecutionListener; import org.junit.platform.launcher.listeners.NoopTestExecutionListener; @@ -46,24 +62,41 @@ void preconditions() { } @Test - void noopTestExecutionListenerIsLoadedViaServiceApi() { + void testExecutionListenerIsLoadedViaServiceApi() { withTestServices(() -> { - var launcher = (InternalLauncher) LauncherFactory.create(); - var listeners = launcher.getTestExecutionListenerRegistry().getListeners(); - var listener = listeners.stream().filter(NoopTestExecutionListener.class::isInstance).findFirst(); - assertThat(listener).isPresent(); + var config = LauncherConfig.builder() // + .addTestEngines(new TestEngineSpy()) // + .enableTestEngineAutoRegistration(false) // + .build(); + var launcher = LauncherFactory.create(config); + + NoopTestExecutionListener.called = false; + + launcher.execute(request().build()); + + assertTrue(NoopTestExecutionListener.called); }); } @Test - void unusedTestExecutionListenerIsNotLoadedViaServiceApi() { + void testExecutionListenersExcludedViaConfigParametersIsNotLoadedViaServiceApi() { withTestServices(() -> { - var launcher = (InternalLauncher) LauncherFactory.create(); - var listeners = launcher.getTestExecutionListenerRegistry().getListeners(); - - assertThat(listeners).filteredOn(AnotherUnusedTestExecutionListener.class::isInstance).isEmpty(); - assertThat(listeners).filteredOn(UnusedTestExecutionListener.class::isInstance).isEmpty(); - assertThat(listeners).filteredOn(NoopTestExecutionListener.class::isInstance).isNotEmpty(); + var value = "org.junit.*.launcher.listeners.Unused*,org.junit.*.launcher.listeners.AnotherUnused*"; + withSystemProperty(DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME, value, () -> { + var config = LauncherConfig.builder() // + .addTestEngines(new TestEngineSpy()) // + .enableTestEngineAutoRegistration(false) // + .build(); + var launcher = LauncherFactory.create(config); + + UnusedTestExecutionListener.called = false; + AnotherUnusedTestExecutionListener.called = false; + + launcher.execute(request().build()); + + assertFalse(UnusedTestExecutionListener.called); + assertFalse(AnotherUnusedTestExecutionListener.called); + }); }); } @@ -124,12 +157,7 @@ void createWithPostDiscoveryFilters() { @Test void applyPostDiscoveryFiltersViaServiceApi() { - final var current = Thread.currentThread().getContextClassLoader(); - try { - var url = getClass().getClassLoader().getResource("testservices/"); - var classLoader = new URLClassLoader(new URL[] { url }, current); - Thread.currentThread().setContextClassLoader(classLoader); - + withTestServices(() -> { var discoveryRequest = createLauncherDiscoveryRequestForBothStandardEngineExampleClasses(); var config = LauncherConfig.builder()// @@ -141,20 +169,12 @@ void applyPostDiscoveryFiltersViaServiceApi() { final var jupiter = testPlan.getChildren(UniqueId.parse("[engine:junit-jupiter]")); assertThat(jupiter).hasSize(1); - } - finally { - Thread.currentThread().setContextClassLoader(current); - } + }); } @Test void notApplyIfDisabledPostDiscoveryFiltersViaServiceApi() { - final var current = Thread.currentThread().getContextClassLoader(); - try { - var url = getClass().getClassLoader().getResource("testservices/"); - var classLoader = new URLClassLoader(new URL[] { url }, current); - Thread.currentThread().setContextClassLoader(classLoader); - + withTestServices(() -> { var discoveryRequest = createLauncherDiscoveryRequestForBothStandardEngineExampleClasses(); var config = LauncherConfig.builder()// @@ -166,62 +186,165 @@ void notApplyIfDisabledPostDiscoveryFiltersViaServiceApi() { final var jupiter = testPlan.getChildren(UniqueId.parse("[engine:junit-jupiter]")); assertThat(jupiter).hasSize(1); - } - finally { - Thread.currentThread().setContextClassLoader(current); - } + }); } @Test void doesNotDiscoverLauncherDiscoverRequestListenerViaServiceApiWhenDisabled() { withTestServices(() -> { - var launcher = (InternalLauncher) LauncherFactory.create( - LauncherConfig.builder().enableLauncherDiscoveryListenerAutoRegistration(false).build()); - var launcherDiscoveryListener = launcher.getLauncherDiscoveryListenerRegistry().getCompositeListener(); + var config = LauncherConfig.builder() // + .enableLauncherDiscoveryListenerAutoRegistration(false) // + .build(); + var launcher = LauncherFactory.create(config); + TestLauncherDiscoveryListener.called = false; + + launcher.discover(request().build()); - assertThat(launcherDiscoveryListener).isSameAs(LauncherDiscoveryListener.NOOP); + assertFalse(TestLauncherDiscoveryListener.called); }); } @Test void discoversLauncherDiscoverRequestListenerViaServiceApiByDefault() { withTestServices(() -> { - var launcher = (InternalLauncher) LauncherFactory.create(); - var launcherDiscoveryListener = launcher.getLauncherDiscoveryListenerRegistry().getCompositeListener(); + var launcher = LauncherFactory.create(); + TestLauncherDiscoveryListener.called = false; - assertThat(launcherDiscoveryListener.getClass().getSimpleName()).startsWith("Composite"); - assertThat(launcherDiscoveryListener).extracting("listeners").asList() // - .contains(new TestLauncherDiscoveryListener()); + launcher.discover(request().build()); + + assertTrue(TestLauncherDiscoveryListener.called); }); } @Test void doesNotDiscoverLauncherSessionListenerViaServiceApiWhenDisabled() { withTestServices(() -> { - var session = (DefaultLauncherSession) LauncherFactory.openSession( - LauncherConfig.builder().enableLauncherSessionListenerAutoRegistration(false).build()); + try (var session = (DefaultLauncherSession) LauncherFactory.openSession( + LauncherConfig.builder().enableLauncherSessionListenerAutoRegistration(false).build())) { - assertThat(session.getListener()).isSameAs(LauncherSessionListener.NOOP); + assertThat(session.getListener()).isSameAs(LauncherSessionListener.NOOP); + } }); } @Test void discoversLauncherSessionListenerViaServiceApiByDefault() { withTestServices(() -> { - var session = (DefaultLauncherSession) LauncherFactory.openSession(); - - assertThat(session.getListener()).isEqualTo(new TestLauncherSessionListener()); + try (var session = (DefaultLauncherSession) LauncherFactory.openSession()) { + assertThat(session.getListener()).isEqualTo(new TestLauncherSessionListener()); + } }); } + @Test + void createsLauncherInterceptorsBeforeDiscoveringTestEngines() { + withTestServices(() -> withSystemProperty(ENABLE_LAUNCHER_INTERCEPTORS, "true", () -> { + var config = LauncherConfig.builder() // + .enableTestEngineAutoRegistration(true) // + .build(); + var request = request().build(); + + var testPlan = LauncherFactory.create(config).discover(request); + + assertThat(testPlan.getRoots()) // + .map(TestIdentifier::getUniqueIdObject) // + .map(UniqueId::getLastSegment) // + .map(UniqueId.Segment::getValue) // + .describedAs( + "Intercepted test engine is added by class loader created by TestLauncherInterceptor1").contains( + InterceptedTestEngine.ID); + })); + } + + @Test + void appliesLauncherInterceptorsToTestDiscovery() { + InterceptorInjectedLauncherSessionListener.CALLS = 0; + withTestServices(() -> withSystemProperty(ENABLE_LAUNCHER_INTERCEPTORS, "true", () -> { + var engine = new TestEngineSpy() { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + throw new RuntimeException("from discovery"); + } + }; + var config = LauncherConfig.builder() // + .enableTestEngineAutoRegistration(false) // + .addTestEngines(engine) // + .build(); + var launcher = LauncherFactory.create(config); + var request = request().build(); + + var exception = assertThrows(RuntimeException.class, () -> launcher.discover(request)); + + assertThat(exception) // + .hasRootCauseMessage("from discovery") // + .hasStackTraceContaining(TestLauncherInterceptor1.class.getName() + ".intercept(") // + .hasStackTraceContaining(TestLauncherInterceptor2.class.getName() + ".intercept("); + assertThat(InterceptorInjectedLauncherSessionListener.CALLS).isEqualTo(1); + })); + } + + @Test + void appliesLauncherInterceptorsToTestExecution() { + InterceptorInjectedLauncherSessionListener.CALLS = 0; + withTestServices(() -> withSystemProperty(ENABLE_LAUNCHER_INTERCEPTORS, "true", () -> { + var engine = new TestEngineSpy() { + @Override + public void execute(ExecutionRequest request) { + throw new RuntimeException("from execution"); + } + }; + var config = LauncherConfig.builder() // + .enableTestEngineAutoRegistration(false) // + .addTestEngines(engine) // + .build(); + var launcher = LauncherFactory.create(config); + var request = request().build(); + + AtomicReference result = new AtomicReference<>(); + launcher.execute(request, new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (testIdentifier.getParentId().isEmpty()) { + result.set(testExecutionResult); + } + } + }); + + assertThat(result.get().getThrowable().orElseThrow()) // + .hasRootCauseMessage("from execution") // + .hasStackTraceContaining(TestLauncherInterceptor1.class.getName() + ".intercept(") // + .hasStackTraceContaining(TestLauncherInterceptor2.class.getName() + ".intercept("); + assertThat(InterceptorInjectedLauncherSessionListener.CALLS).isEqualTo(1); + })); + } + + @SuppressWarnings("SameParameterValue") + private static void withSystemProperty(String key, String value, Runnable runnable) { + var oldValue = System.getProperty(key); + System.setProperty(key, value); + try { + runnable.run(); + } + finally { + if (oldValue == null) { + System.clearProperty(key); + } + else { + System.setProperty(key, oldValue); + } + } + } + private static void withTestServices(Runnable runnable) { var current = Thread.currentThread().getContextClassLoader(); - try { - var url = LauncherFactoryTests.class.getClassLoader().getResource("testservices/"); - var classLoader = new URLClassLoader(new URL[] { url }, current); + var url = LauncherFactoryTests.class.getClassLoader().getResource("testservices/"); + try (var classLoader = new URLClassLoader(new URL[] { url }, current)) { Thread.currentThread().setContextClassLoader(classLoader); runnable.run(); } + catch (IOException e) { + throw new UncheckedIOException(e); + } finally { Thread.currentThread().setContextClassLoader(current); } @@ -236,6 +359,7 @@ private LauncherDiscoveryRequest createLauncherDiscoveryRequestForBothStandardEn // @formatter:on } + @SuppressWarnings("NewClassNamingConvention") public static class JUnit4Example { @org.junit.Test @@ -244,6 +368,7 @@ public void testJ4() { } + @SuppressWarnings("NewClassNamingConvention") static class JUnit5Example { @Tag("test-post-discovery") diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/AnotherUnusedTestExecutionListener.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/AnotherUnusedTestExecutionListener.java index c3d6cfaccd8a..a0ca73ca6361 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/AnotherUnusedTestExecutionListener.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/AnotherUnusedTestExecutionListener.java @@ -11,7 +11,13 @@ package org.junit.platform.launcher.listeners; import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; public class AnotherUnusedTestExecutionListener implements TestExecutionListener { - // empty on purpose + public static boolean called; + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + called = true; + } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/NoopTestExecutionListener.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/NoopTestExecutionListener.java index d471e6e745c2..268dd599c80f 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/NoopTestExecutionListener.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/NoopTestExecutionListener.java @@ -11,10 +11,16 @@ package org.junit.platform.launcher.listeners; import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; /** * @since 1.0 */ public class NoopTestExecutionListener implements TestExecutionListener { - // empty on purpose + public static boolean called; + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + called = true; + } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/UnusedTestExecutionListener.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/UnusedTestExecutionListener.java index cbf7e6f9bea9..53353c2ff3ff 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/UnusedTestExecutionListener.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/UnusedTestExecutionListener.java @@ -11,7 +11,13 @@ package org.junit.platform.launcher.listeners; import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; public class UnusedTestExecutionListener implements TestExecutionListener { - // empty on purpose + public static boolean called; + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + called = true; + } } diff --git a/platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.engine.TestEngine b/platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.engine.TestEngine new file mode 100644 index 000000000000..c27ba1c33eee --- /dev/null +++ b/platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.engine.TestEngine @@ -0,0 +1 @@ +org.junit.platform.launcher.InterceptedTestEngine diff --git a/platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 000000000000..170360744b1c --- /dev/null +++ b/platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +org.junit.platform.launcher.InterceptorInjectedLauncherSessionListener diff --git a/platform-tests/src/test/resources/testservices/META-INF/services/org.junit.platform.launcher.LauncherInterceptor b/platform-tests/src/test/resources/testservices/META-INF/services/org.junit.platform.launcher.LauncherInterceptor new file mode 100644 index 000000000000..2aa385ca1c2f --- /dev/null +++ b/platform-tests/src/test/resources/testservices/META-INF/services/org.junit.platform.launcher.LauncherInterceptor @@ -0,0 +1,2 @@ +org.junit.platform.launcher.TestLauncherInterceptor1 +org.junit.platform.launcher.TestLauncherInterceptor2 diff --git a/platform-tests/src/test/resources/testservices/junit-platform.properties b/platform-tests/src/test/resources/testservices/junit-platform.properties deleted file mode 100644 index 7b87d3ca34ff..000000000000 --- a/platform-tests/src/test/resources/testservices/junit-platform.properties +++ /dev/null @@ -1 +0,0 @@ -junit.platform.execution.listeners.deactivate=org.junit.*.launcher.listeners.Unused*,org.junit.*.launcher.listeners.AnotherUnused* \ No newline at end of file diff --git a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts index edd8bd7b7474..cd346c7f1ac5 100644 --- a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts +++ b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts @@ -27,3 +27,12 @@ tasks.test { ) } } + +graalvmNative { + binaries { + named("test") { + buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig") + buildArgs.add("-H:+ReportExceptionStackTraces") + } + } +} diff --git a/platform-tooling-support-tests/projects/standalone/expected-err.txt b/platform-tooling-support-tests/projects/standalone/expected-err.txt index 46cdad9ec4b2..cc4b8df06ddd 100644 --- a/platform-tooling-support-tests/projects/standalone/expected-err.txt +++ b/platform-tooling-support-tests/projects/standalone/expected-err.txt @@ -1,3 +1,7 @@ +.+ org.junit.platform.launcher.core.ServiceLoaderRegistry load +.+ Loaded LauncherInterceptor instances: .. +.+ org.junit.platform.launcher.core.ServiceLoaderRegistry load +.+ Loaded LauncherSessionListener instances: .. .+ org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines .+ Discovered TestEngines: - junit-jupiter .+ @@ -9,8 +13,6 @@ .+ Loaded LauncherDiscoveryListener instances: .. .+ org.junit.platform.launcher.core.ServiceLoaderRegistry load .+ Loaded TestExecutionListener instances: .+ -.+ org.junit.platform.launcher.core.ServiceLoaderRegistry load -.+ Loaded LauncherSessionListener instances: .. .+ org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines .+ Discovered TestEngines: - junit-jupiter .+ diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java index 6bd095431836..97f322765d28 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java @@ -120,6 +120,7 @@ void test() throws IOException { .addArguments("--show-version") // .addArguments("-enableassertions") // .addArguments("-Djava.util.logging.config.file=logging.properties") // + .addArguments("-Djunit.platform.launcher.interceptors.enabled=true") // .addArguments("-jar", MavenRepo.jar("junit-platform-console-standalone")) // .addArguments("--scan-class-path") // .addArguments("--disable-banner") // @@ -153,6 +154,7 @@ void testOnJava8() throws IOException { .addArguments("--show-version") // .addArguments("-enableassertions") // .addArguments("-Djava.util.logging.config.file=logging.properties") // + .addArguments("-Djunit.platform.launcher.interceptors.enabled=true") // .addArguments("-jar", MavenRepo.jar("junit-platform-console-standalone")) // .addArguments("--scan-class-path") // .addArguments("--disable-banner") // @@ -187,6 +189,7 @@ void testOnJava8SelectPackage() throws IOException { .addArguments("--show-version") // .addArguments("-enableassertions") // .addArguments("-Djava.util.logging.config.file=logging.properties") // + .addArguments("-Djunit.platform.launcher.interceptors.enabled=true") // .addArguments("-jar", MavenRepo.jar("junit-platform-console-standalone")) // .addArguments("--select-package", "standalone") // .addArguments("--disable-banner") //