From f6dd7a26641edbd631a37be72088d5671dd92fad Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 14 Nov 2022 08:16:12 +0100 Subject: [PATCH 1/7] Remove duplication --- .../launcher/core/LauncherFactoryTests.java | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) 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..5b6368f79eaf 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 @@ -124,12 +124,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 +136,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,10 +153,7 @@ void notApplyIfDisabledPostDiscoveryFiltersViaServiceApi() { final var jupiter = testPlan.getChildren(UniqueId.parse("[engine:junit-jupiter]")); assertThat(jupiter).hasSize(1); - } - finally { - Thread.currentThread().setContextClassLoader(current); - } + }); } @Test From 45f9d0c24a372eb4b1df4024918dfe6dee2d20de Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 11 Nov 2022 16:29:56 +0100 Subject: [PATCH 2/7] Introduce LauncherInterceptor --- .../platform/launcher/LauncherConstants.java | 2 + .../launcher/LauncherInterceptor.java | 57 ++++++++++++ .../core/CloseableInternalLauncher.java | 19 ++++ .../launcher/core/DefaultLauncherSession.java | 71 +++++++-------- .../DelegatingCloseableInternalLauncher.java | 30 +++++++ .../core/DelegatingInternalLauncher.java | 63 ++++++++++++++ .../core/InterceptingInternalLauncher.java | 86 +++++++++++++++++++ .../launcher/core/LauncherFactory.java | 36 ++++++-- .../core/SessionPerRequestLauncher.java | 37 +++----- .../launcher/TestLauncherInterceptor1.java | 23 +++++ .../launcher/TestLauncherInterceptor2.java | 23 +++++ .../launcher/core/LauncherFactoryTests.java | 84 ++++++++++++++++++ ...unit.platform.launcher.LauncherInterceptor | 2 + 13 files changed, 461 insertions(+), 72 deletions(-) create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherInterceptor.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableInternalLauncher.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java create mode 100644 platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java create mode 100644 platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java create mode 100644 platform-tests/src/test/resources/testservices/META-INF/services/org.junit.platform.launcher.LauncherInterceptor 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..218bf3a077ed 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 @@ -135,6 +135,8 @@ public class LauncherConstants { */ public static final String DEACTIVATE_ALL_LISTENERS_PATTERN = ClassNamePatternFilterUtils.DEACTIVATE_ALL_PATTERN; + 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..cf91b3d67d10 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherInterceptor.java @@ -0,0 +1,57 @@ +/* + * 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}. + * + *

Interceptors are instantiated once per {@link LauncherSession} and closed + * when the session is about to be closed. + * + * @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/CloseableInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableInternalLauncher.java new file mode 100644 index 000000000000..110057708947 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableInternalLauncher.java @@ -0,0 +1,19 @@ +/* + * 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; + +/** + * @since 1.10 + */ +interface CloseableInternalLauncher extends InternalLauncher, AutoCloseable { + @Override + void close(); +} 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..a8fa1150fa5f 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,13 @@ package org.junit.platform.launcher.core; +import java.util.List; + 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; @@ -27,8 +30,9 @@ class DefaultLauncherSession implements LauncherSession { private final DelegatingLauncher launcher; private final LauncherSessionListener listener; - DefaultLauncherSession(Launcher launcher, LauncherSessionListener listener) { - this.launcher = new DelegatingLauncher(launcher); + DefaultLauncherSession(InternalLauncher launcher, LauncherSessionListener listener, + List interceptors) { + this.launcher = new DelegatingLauncher(InterceptingInternalLauncher.decorate(launcher, interceptors)); this.listener = listener; listener.launcherSessionOpened(this); } @@ -44,55 +48,31 @@ LauncherSessionListener getListener() { @Override public void close() { - if (launcher.getDelegate() != ClosedLauncher.INSTANCE) { - launcher.setDelegate(ClosedLauncher.INSTANCE); + if (launcher.isClosed()) { + launcher.close(); 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); - } + private static class DelegatingLauncher extends DelegatingInternalLauncher + implements CloseableInternalLauncher { - @Override - public TestPlan discover(LauncherDiscoveryRequest launcherDiscoveryRequest) { - return delegate.discover(launcherDiscoveryRequest); + DelegatingLauncher(CloseableInternalLauncher delegate) { + super(delegate); } - @Override - public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners) { - delegate.execute(launcherDiscoveryRequest, listeners); + boolean isClosed() { + return delegate != ClosedLauncher.INSTANCE; } @Override - public void execute(TestPlan testPlan, TestExecutionListener... listeners) { - delegate.execute(testPlan, listeners); + public void close() { + delegate.close(); + delegate = ClosedLauncher.INSTANCE; } } - private static class ClosedLauncher implements Launcher { + private static class ClosedLauncher implements CloseableInternalLauncher { static final ClosedLauncher INSTANCE = new ClosedLauncher(); @@ -123,5 +103,20 @@ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecu public void execute(TestPlan testPlan, TestExecutionListener... listeners) { throw new PreconditionViolationException("Launcher session has already been closed"); } + + @Override + public ListenerRegistry getTestExecutionListenerRegistry() { + throw new PreconditionViolationException("Launcher session has already been closed"); + } + + @Override + public ListenerRegistry getLauncherDiscoveryListenerRegistry() { + throw new PreconditionViolationException("Launcher session has already been closed"); + } + + @Override + public void close() { + // do nothing + } } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java new file mode 100644 index 000000000000..1282cf7ac7d9 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java @@ -0,0 +1,30 @@ +/* + * 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; + +/** + * @since 1.10 + */ +class DelegatingCloseableInternalLauncher extends DelegatingInternalLauncher + implements CloseableInternalLauncher { + + private final Runnable onClose; + + public DelegatingCloseableInternalLauncher(T delegate, Runnable onClose) { + super(delegate); + this.onClose = onClose; + } + + @Override + public final void close() { + onClose.run(); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java new file mode 100644 index 000000000000..c4078ff94ae8 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java @@ -0,0 +1,63 @@ +/* + * 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.LauncherDiscoveryListener; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; + +/** + * @since 1.10 + */ +class DelegatingInternalLauncher implements InternalLauncher { + + protected T delegate; + + DelegatingInternalLauncher(T 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); + } + + @Override + public ListenerRegistry getTestExecutionListenerRegistry() { + return delegate.getTestExecutionListenerRegistry(); + } + + @Override + public ListenerRegistry getLauncherDiscoveryListenerRegistry() { + return delegate.getLauncherDiscoveryListenerRegistry(); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java new file mode 100644 index 000000000000..4524a88fb5de --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java @@ -0,0 +1,86 @@ +/* + * 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 java.util.List; +import java.util.Optional; + +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 InterceptingInternalLauncher extends DelegatingCloseableInternalLauncher { + + static CloseableInternalLauncher decorate(InternalLauncher launcher, List interceptors) { + return composite(interceptors) // + .map(combinedInterceptor -> (CloseableInternalLauncher) new InterceptingInternalLauncher(launcher, + combinedInterceptor)) // + .orElse(new DelegatingCloseableInternalLauncher<>(launcher, () -> { + // do nothing + })); + } + + private static Optional composite(List interceptors) { + if (interceptors.isEmpty()) { + return Optional.empty(); + } + return Optional.of(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)); + } + })); + } + + private final LauncherInterceptor interceptor; + + private InterceptingInternalLauncher(InternalLauncher delegate, LauncherInterceptor interceptor) { + super(delegate, interceptor::close); + 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..140cb4c58632 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; @@ -95,7 +98,9 @@ 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)); + LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); + return new DefaultLauncherSession(createDefaultLauncher(config, configurationParameters), + createLauncherSessionListener(config), collectLauncherInterceptors(configurationParameters)); } /** @@ -122,10 +127,13 @@ 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)); + LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); + return new SessionPerRequestLauncher(createDefaultLauncher(config, configurationParameters), + createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); } - private static DefaultLauncher createDefaultLauncher(LauncherConfig config) { + private static DefaultLauncher createDefaultLauncher(LauncherConfig config, + LauncherConfigurationParameters configurationParameters) { Preconditions.notNull(config, "LauncherConfig must not be null"); Set engines = collectTestEngines(config); @@ -134,11 +142,21 @@ private static DefaultLauncher createDefaultLauncher(LauncherConfig 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<>(); + SERVICE_LOADER_REGISTRY.load(LauncherInterceptor.class).forEach(interceptors::add); + return interceptors; + } + return emptyList(); + } + private static Set collectTestEngines(LauncherConfig config) { Set engines = new LinkedHashSet<>(); if (config.isTestEngineAutoRegistrationEnabled()) { @@ -174,16 +192,18 @@ private static void registerLauncherDiscoveryListeners(LauncherConfig config, La 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() { + private static Stream loadAndFilterTestExecutionListeners( + ConfigurationParameters configurationParameters) { Iterable listeners = SERVICE_LOADER_REGISTRY.load(TestExecutionListener.class); - ConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); 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/SessionPerRequestLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java index 9c0b7dd47e60..7de4df66d5e7 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,11 @@ package org.junit.platform.launcher.core; -import org.junit.platform.launcher.LauncherDiscoveryListener; +import java.util.List; +import java.util.function.Supplier; + 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 +23,16 @@ /** * @since 1.8 */ -class SessionPerRequestLauncher implements InternalLauncher { +class SessionPerRequestLauncher extends DelegatingInternalLauncher { - private final InternalLauncher delegate; private final LauncherSessionListener sessionListener; + private final Supplier> interceptorFactory; - SessionPerRequestLauncher(InternalLauncher delegate, LauncherSessionListener sessionListener) { - this.delegate = delegate; + SessionPerRequestLauncher(InternalLauncher delegate, LauncherSessionListener sessionListener, + Supplier> interceptorFactory) { + super(delegate); this.sessionListener = sessionListener; - } - - @Override - public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) { - delegate.registerLauncherDiscoveryListeners(listeners); - } - - @Override - public void registerTestExecutionListeners(TestExecutionListener... listeners) { - delegate.registerTestExecutionListeners(listeners); + this.interceptorFactory = interceptorFactory; } @Override @@ -61,17 +56,7 @@ 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); + return new DefaultLauncherSession(delegate, sessionListener, interceptorFactory.get()); } } 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..7bd8f95ff690 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java @@ -0,0 +1,23 @@ +/* + * 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 TestLauncherInterceptor1 implements LauncherInterceptor { + + @Override + public T intercept(Invocation invocation) { + return invocation.proceed(); + } + + @Override + public void close() { + } +} 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..bdc198e3ee06 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java @@ -0,0 +1,23 @@ +/* + * 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 { + + @Override + public T intercept(Invocation invocation) { + return invocation.proceed(); + } + + @Override + public void close() { + } +} 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 5b6368f79eaf..36baca65a79d 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 @@ -14,22 +14,32 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.LauncherConstants.ENABLE_LAUNCHER_INTERCEPTORS; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; 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.fakes.TestEngineSpy; import org.junit.platform.launcher.LauncherDiscoveryListener; 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; @@ -198,6 +208,80 @@ void discoversLauncherSessionListenerViaServiceApiByDefault() { }); } + @Test + void appliesLauncherInterceptorsToTestDiscovery() { + 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("); + })); + } + + @Test + void appliesLauncherInterceptorsToTestExecution() { + 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("); + })); + } + + 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 { 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 From 09785dda37b06f12dad535e64619cea74b84a058 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 15 Nov 2022 21:14:23 +0100 Subject: [PATCH 3/7] Create interceptors before launcher So they can influence the class loader used to load test engines etc. --- .../junit/platform/fakes/TestEngineSpy.java | 16 ++- ...alLauncher.java => CloseableLauncher.java} | 4 +- .../launcher/core/DefaultLauncher.java | 23 +--- .../launcher/core/DefaultLauncherSession.java | 45 ++----- .../DelegatingCloseableInternalLauncher.java | 30 ----- .../core/DelegatingCloseableLauncher.java | 41 +++++++ ...lLauncher.java => DelegatingLauncher.java} | 14 +-- ...java => InterceptingClosableLauncher.java} | 27 +++-- .../launcher/core/LauncherFactory.java | 10 +- ...her.java => LauncherListenerRegistry.java} | 12 +- .../core/SessionPerRequestLauncher.java | 28 ++++- .../launcher/InterceptedTestEngine.java | 22 ++++ .../TestLauncherDiscoveryListener.java | 16 +-- .../launcher/TestLauncherInterceptor1.java | 24 ++++ .../launcher/core/DefaultLauncherTests.java | 21 ++-- .../launcher/core/LauncherFactoryTests.java | 112 +++++++++++++----- .../AnotherUnusedTestExecutionListener.java | 8 +- .../listeners/NoopTestExecutionListener.java | 8 +- .../UnusedTestExecutionListener.java | 8 +- .../org.junit.platform.engine.TestEngine | 1 + .../testservices/junit-platform.properties | 1 - .../projects/standalone/expected-err.txt | 4 +- 22 files changed, 293 insertions(+), 182 deletions(-) rename junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/{CloseableInternalLauncher.java => CloseableLauncher.java} (79%) delete mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java rename junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/{DelegatingInternalLauncher.java => DelegatingLauncher.java} (76%) rename junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/{InterceptingInternalLauncher.java => InterceptingClosableLauncher.java} (68%) rename junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/{InternalLauncher.java => LauncherListenerRegistry.java} (63%) create mode 100644 platform-tests/src/test/java/org/junit/platform/launcher/InterceptedTestEngine.java create mode 100644 platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.engine.TestEngine delete mode 100644 platform-tests/src/test/resources/testservices/junit-platform.properties 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/core/CloseableInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableLauncher.java similarity index 79% rename from junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableInternalLauncher.java rename to junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableLauncher.java index 110057708947..1cc023471b22 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableInternalLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableLauncher.java @@ -10,10 +10,12 @@ package org.junit.platform.launcher.core; +import org.junit.platform.launcher.Launcher; + /** * @since 1.10 */ -interface CloseableInternalLauncher extends InternalLauncher, AutoCloseable { +interface CloseableLauncher extends Launcher, AutoCloseable { @Override void close(); } 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 a8fa1150fa5f..0a4436dd5ed1 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 @@ -11,6 +11,7 @@ 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; @@ -27,12 +28,16 @@ */ class DefaultLauncherSession implements LauncherSession { - private final DelegatingLauncher launcher; + private final DelegatingCloseableLauncher launcher; private final LauncherSessionListener listener; - DefaultLauncherSession(InternalLauncher launcher, LauncherSessionListener listener, - List interceptors) { - this.launcher = new DelegatingLauncher(InterceptingInternalLauncher.decorate(launcher, interceptors)); + DefaultLauncherSession(Supplier launcherSupplier, List interceptors, + LauncherSessionListener listener) { + CloseableLauncher closeableLauncher = InterceptingClosableLauncher.decorate(launcherSupplier, interceptors); + this.launcher = new DelegatingCloseableLauncher<>(closeableLauncher, delegate -> { + delegate.close(); + return ClosedLauncher.INSTANCE; + }); this.listener = listener; listener.launcherSessionOpened(this); } @@ -48,31 +53,13 @@ LauncherSessionListener getListener() { @Override public void close() { - if (launcher.isClosed()) { + if (!launcher.isClosed()) { launcher.close(); listener.launcherSessionClosed(this); } } - private static class DelegatingLauncher extends DelegatingInternalLauncher - implements CloseableInternalLauncher { - - DelegatingLauncher(CloseableInternalLauncher delegate) { - super(delegate); - } - - boolean isClosed() { - return delegate != ClosedLauncher.INSTANCE; - } - - @Override - public void close() { - delegate.close(); - delegate = ClosedLauncher.INSTANCE; - } - } - - private static class ClosedLauncher implements CloseableInternalLauncher { + private static class ClosedLauncher implements CloseableLauncher { static final ClosedLauncher INSTANCE = new ClosedLauncher(); @@ -104,16 +91,6 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { throw new PreconditionViolationException("Launcher session has already been closed"); } - @Override - public ListenerRegistry getTestExecutionListenerRegistry() { - throw new PreconditionViolationException("Launcher session has already been closed"); - } - - @Override - public ListenerRegistry getLauncherDiscoveryListenerRegistry() { - throw new PreconditionViolationException("Launcher session has already been closed"); - } - @Override public void close() { // do nothing diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java deleted file mode 100644 index 1282cf7ac7d9..000000000000 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableInternalLauncher.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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; - -/** - * @since 1.10 - */ -class DelegatingCloseableInternalLauncher extends DelegatingInternalLauncher - implements CloseableInternalLauncher { - - private final Runnable onClose; - - public DelegatingCloseableInternalLauncher(T delegate, Runnable onClose) { - super(delegate); - this.onClose = onClose; - } - - @Override - public final void close() { - onClose.run(); - } -} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java new file mode 100644 index 000000000000..a6ea513acf03 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java @@ -0,0 +1,41 @@ +/* + * 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 java.util.function.Function; + +import org.junit.platform.launcher.Launcher; + +/** + * @since 1.10 + */ +class DelegatingCloseableLauncher extends DelegatingLauncher implements CloseableLauncher { + + private final Function onClose; + private boolean closed; + + public DelegatingCloseableLauncher(T delegate, Function onClose) { + super(delegate); + this.onClose = onClose; + } + + @Override + public final void close() { + if (!closed) { + this.closed = true; + delegate = this.onClose.apply(this.delegate); + } + } + + public boolean isClosed() { + return closed; + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java similarity index 76% rename from junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java rename to junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java index c4078ff94ae8..3e226af1255d 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingInternalLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java @@ -10,6 +10,7 @@ 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; @@ -18,11 +19,11 @@ /** * @since 1.10 */ -class DelegatingInternalLauncher implements InternalLauncher { +class DelegatingLauncher implements Launcher { protected T delegate; - DelegatingInternalLauncher(T delegate) { + DelegatingLauncher(T delegate) { this.delegate = delegate; } @@ -51,13 +52,4 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { delegate.execute(testPlan, listeners); } - @Override - public ListenerRegistry getTestExecutionListenerRegistry() { - return delegate.getTestExecutionListenerRegistry(); - } - - @Override - public ListenerRegistry getLauncherDiscoveryListenerRegistry() { - return delegate.getLauncherDiscoveryListenerRegistry(); - } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java similarity index 68% rename from junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java rename to junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java index 4524a88fb5de..20dbf2f6ed06 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingInternalLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java @@ -10,9 +10,13 @@ package org.junit.platform.launcher.core; +import static java.util.function.Function.identity; + import java.util.List; import java.util.Optional; +import java.util.function.Supplier; +import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.LauncherInterceptor; import org.junit.platform.launcher.TestExecutionListener; @@ -21,15 +25,15 @@ /** * @since 1.10 */ -class InterceptingInternalLauncher extends DelegatingCloseableInternalLauncher { +class InterceptingClosableLauncher extends DelegatingCloseableLauncher { - static CloseableInternalLauncher decorate(InternalLauncher launcher, List interceptors) { - return composite(interceptors) // - .map(combinedInterceptor -> (CloseableInternalLauncher) new InterceptingInternalLauncher(launcher, - combinedInterceptor)) // - .orElse(new DelegatingCloseableInternalLauncher<>(launcher, () -> { - // do nothing - })); + static CloseableLauncher decorate(Supplier launcherSupplier, List interceptors) { + Optional combinedInterceptor = composite(interceptors); + Launcher launcher = combinedInterceptor.map(it -> it.intercept(launcherSupplier::get)).orElseGet( + launcherSupplier); + return combinedInterceptor // + .map(it -> (CloseableLauncher) new InterceptingClosableLauncher(launcher, it)) // + .orElse(new DelegatingCloseableLauncher<>(launcher, identity())); } private static Optional composite(List interceptors) { @@ -58,8 +62,11 @@ public T intercept(Invocation invocation) { private final LauncherInterceptor interceptor; - private InterceptingInternalLauncher(InternalLauncher delegate, LauncherInterceptor interceptor) { - super(delegate, interceptor::close); + private InterceptingClosableLauncher(Launcher delegate, LauncherInterceptor interceptor) { + super(delegate, it -> { + interceptor.close(); + return it; + }); this.interceptor = interceptor; } 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 140cb4c58632..77edab9bbc67 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 @@ -98,9 +98,10 @@ public static LauncherSession openSession() throws PreconditionViolationExceptio */ @API(status = EXPERIMENTAL, since = "1.8") public static LauncherSession openSession(LauncherConfig config) throws PreconditionViolationException { + Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); - return new DefaultLauncherSession(createDefaultLauncher(config, configurationParameters), - createLauncherSessionListener(config), collectLauncherInterceptors(configurationParameters)); + return new DefaultLauncherSession(() -> createDefaultLauncher(config, configurationParameters), + collectLauncherInterceptors(configurationParameters), createLauncherSessionListener(config)); } /** @@ -127,15 +128,14 @@ public static Launcher create() throws PreconditionViolationException { */ @API(status = EXPERIMENTAL, since = "1.3") public static Launcher create(LauncherConfig config) throws PreconditionViolationException { + Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); - return new SessionPerRequestLauncher(createDefaultLauncher(config, configurationParameters), + return new SessionPerRequestLauncher(() -> createDefaultLauncher(config, configurationParameters), createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); } private static DefaultLauncher createDefaultLauncher(LauncherConfig config, LauncherConfigurationParameters configurationParameters) { - Preconditions.notNull(config, "LauncherConfig must not be null"); - Set engines = collectTestEngines(config); List filters = collectPostDiscoveryFilters(config); 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/SessionPerRequestLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java index 7de4df66d5e7..1955bc625e56 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 @@ -13,6 +13,8 @@ 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; @@ -23,18 +25,30 @@ /** * @since 1.8 */ -class SessionPerRequestLauncher extends DelegatingInternalLauncher { +class SessionPerRequestLauncher implements Launcher { + private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry(); + private final Supplier launcherSupplier; private final LauncherSessionListener sessionListener; private final Supplier> interceptorFactory; - SessionPerRequestLauncher(InternalLauncher delegate, LauncherSessionListener sessionListener, + SessionPerRequestLauncher(Supplier launcherSupplier, LauncherSessionListener sessionListener, Supplier> interceptorFactory) { - super(delegate); + this.launcherSupplier = launcherSupplier; this.sessionListener = sessionListener; this.interceptorFactory = interceptorFactory; } + @Override + public void registerLauncherDiscoveryListeners(LauncherDiscoveryListener... listeners) { + listenerRegistry.launcherDiscoveryListeners.addAll(listeners); + } + + @Override + public void registerTestExecutionListeners(TestExecutionListener... listeners) { + listenerRegistry.testExecutionListeners.addAll(listeners); + } + @Override public TestPlan discover(LauncherDiscoveryRequest launcherDiscoveryRequest) { try (LauncherSession session = createSession()) { @@ -57,6 +71,12 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { } private LauncherSession createSession() { - return new DefaultLauncherSession(delegate, sessionListener, interceptorFactory.get()); + LauncherSession session = new DefaultLauncherSession(launcherSupplier, interceptorFactory.get(), + sessionListener); + 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/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 index 7bd8f95ff690..585ccf74b6c4 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java @@ -10,8 +10,23 @@ 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 { + private final ClassLoader originalClassLoader; + private final URLClassLoader replacedClassLoader; + + public TestLauncherInterceptor1() { + originalClassLoader = Thread.currentThread().getContextClassLoader(); + var url = getClass().getClassLoader().getResource("intercepted-testservices/"); + replacedClassLoader = new URLClassLoader(new URL[] { url }, originalClassLoader); + Thread.currentThread().setContextClassLoader(replacedClassLoader); + } + @Override public T intercept(Invocation invocation) { return invocation.proceed(); @@ -19,5 +34,14 @@ public T intercept(Invocation invocation) { @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/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 36baca65a79d..072c6119e369 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,11 +12,16 @@ 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; @@ -31,7 +36,7 @@ import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.fakes.TestEngineSpy; -import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.launcher.InterceptedTestEngine; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TagFilter; @@ -56,24 +61,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(); + 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; - assertThat(listeners).filteredOn(AnotherUnusedTestExecutionListener.class::isInstance).isEmpty(); - assertThat(listeners).filteredOn(UnusedTestExecutionListener.class::isInstance).isEmpty(); - assertThat(listeners).filteredOn(NoopTestExecutionListener.class::isInstance).isNotEmpty(); + launcher.execute(request().build()); + + assertFalse(UnusedTestExecutionListener.called); + assertFalse(AnotherUnusedTestExecutionListener.called); + }); }); } @@ -169,45 +191,70 @@ void notApplyIfDisabledPostDiscoveryFiltersViaServiceApi() { @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; + + launcher.discover(request().build()); - assertThat(launcherDiscoveryListener.getClass().getSimpleName()).startsWith("Composite"); - assertThat(launcherDiscoveryListener).extracting("listeners").asList() // - .contains(new TestLauncherDiscoveryListener()); + 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() { withTestServices(() -> withSystemProperty(ENABLE_LAUNCHER_INTERCEPTORS, "true", () -> { @@ -266,6 +313,7 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult })); } + @SuppressWarnings("SameParameterValue") private static void withSystemProperty(String key, String value, Runnable runnable) { var oldValue = System.getProperty(key); System.setProperty(key, value); @@ -284,12 +332,14 @@ private static void withSystemProperty(String key, String value, Runnable runnab 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); } @@ -304,6 +354,7 @@ private LauncherDiscoveryRequest createLauncherDiscoveryRequestForBothStandardEn // @formatter:on } + @SuppressWarnings("NewClassNamingConvention") public static class JUnit4Example { @org.junit.Test @@ -312,6 +363,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/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/standalone/expected-err.txt b/platform-tooling-support-tests/projects/standalone/expected-err.txt index 46cdad9ec4b2..5788d5f3db68 100644 --- a/platform-tooling-support-tests/projects/standalone/expected-err.txt +++ b/platform-tooling-support-tests/projects/standalone/expected-err.txt @@ -1,3 +1,5 @@ +.+ org.junit.platform.launcher.core.ServiceLoaderRegistry load +.+ Loaded LauncherSessionListener instances: .. .+ org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines .+ Discovered TestEngines: - junit-jupiter .+ @@ -9,8 +11,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 .+ From 1a1ec4c0d668840a0d1a834019f2c945b62bbcff Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 16 Nov 2022 07:57:54 +0100 Subject: [PATCH 4/7] Load interceptors before session listeners --- .../launcher/LauncherInterceptor.java | 40 ++++++++ .../launcher/core/CloseableLauncher.java | 21 ----- .../launcher/core/DefaultLauncherSession.java | 38 ++++---- .../core/DelegatingCloseableLauncher.java | 41 -------- .../launcher/core/DelegatingLauncher.java | 6 +- .../core/InterceptingClosableLauncher.java | 93 ------------------- .../launcher/core/InterceptingLauncher.java | 51 ++++++++++ .../launcher/core/LauncherFactory.java | 6 +- .../core/SessionPerRequestLauncher.java | 11 ++- ...ceptorInjectedLauncherSessionListener.java | 35 +++++++ .../launcher/TestLauncherInterceptor1.java | 4 +- .../launcher/TestLauncherInterceptor2.java | 10 +- .../launcher/core/LauncherFactoryTests.java | 5 + ....platform.launcher.LauncherSessionListener | 1 + .../projects/standalone/expected-err.txt | 2 + .../support/tests/StandaloneTests.java | 3 + 16 files changed, 181 insertions(+), 186 deletions(-) delete mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableLauncher.java delete mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java delete mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingLauncher.java create mode 100644 platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java create mode 100644 platform-tests/src/test/resources/intercepted-testservices/META-INF/services/org.junit.platform.launcher.LauncherSessionListener 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 index cf91b3d67d10..a8a4111dd327 100644 --- 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 @@ -11,6 +11,9 @@ package org.junit.platform.launcher; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; + +import java.util.List; import org.apiguardian.api.API; @@ -28,6 +31,18 @@ @API(status = EXPERIMENTAL, since = "1.10") public interface LauncherInterceptor { + LauncherInterceptor NOOP = new LauncherInterceptor() { + @Override + public T intercept(Invocation invocation) { + return invocation.proceed(); + } + + @Override + public void close() { + // do nothing + } + }; + /** * Intercept the supplied invocation. * @@ -54,4 +69,29 @@ public interface LauncherInterceptor { interface Invocation { T proceed(); } + + @API(status = INTERNAL, since = "1.10") + static LauncherInterceptor composite(List interceptors) { + if (interceptors.isEmpty()) { + return NOOP; + } + 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/CloseableLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableLauncher.java deleted file mode 100644 index 1cc023471b22..000000000000 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CloseableLauncher.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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; - -/** - * @since 1.10 - */ -interface CloseableLauncher extends Launcher, AutoCloseable { - @Override - void close(); -} 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 0a4436dd5ed1..42969448616e 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 @@ -28,17 +28,23 @@ */ class DefaultLauncherSession implements LauncherSession { - private final DelegatingCloseableLauncher launcher; + private final LauncherInterceptor interceptor; private final LauncherSessionListener listener; - - DefaultLauncherSession(Supplier launcherSupplier, List interceptors, - LauncherSessionListener listener) { - CloseableLauncher closeableLauncher = InterceptingClosableLauncher.decorate(launcherSupplier, interceptors); - this.launcher = new DelegatingCloseableLauncher<>(closeableLauncher, delegate -> { - delegate.close(); - return ClosedLauncher.INSTANCE; - }); - this.listener = listener; + private final DelegatingLauncher launcher; + + DefaultLauncherSession(List interceptors, Supplier listenerSupplier, + Supplier launcherSupplier) { + interceptor = LauncherInterceptor.composite(interceptors); + Launcher launcher; + if (interceptor == LauncherInterceptor.NOOP) { + 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); listener.launcherSessionOpened(this); } @@ -53,13 +59,14 @@ LauncherSessionListener getListener() { @Override public void close() { - if (!launcher.isClosed()) { - launcher.close(); + if (launcher.delegate != ClosedLauncher.INSTANCE) { + launcher.delegate = ClosedLauncher.INSTANCE; listener.launcherSessionClosed(this); + interceptor.close(); } } - private static class ClosedLauncher implements CloseableLauncher { + private static class ClosedLauncher implements Launcher { static final ClosedLauncher INSTANCE = new ClosedLauncher(); @@ -90,10 +97,5 @@ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecu public void execute(TestPlan testPlan, TestExecutionListener... listeners) { throw new PreconditionViolationException("Launcher session has already been closed"); } - - @Override - public void close() { - // do nothing - } } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java deleted file mode 100644 index a6ea513acf03..000000000000 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingCloseableLauncher.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 java.util.function.Function; - -import org.junit.platform.launcher.Launcher; - -/** - * @since 1.10 - */ -class DelegatingCloseableLauncher extends DelegatingLauncher implements CloseableLauncher { - - private final Function onClose; - private boolean closed; - - public DelegatingCloseableLauncher(T delegate, Function onClose) { - super(delegate); - this.onClose = onClose; - } - - @Override - public final void close() { - if (!closed) { - this.closed = true; - delegate = this.onClose.apply(this.delegate); - } - } - - public boolean isClosed() { - return closed; - } -} 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 index 3e226af1255d..d4332b52fe35 100644 --- 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 @@ -19,11 +19,11 @@ /** * @since 1.10 */ -class DelegatingLauncher implements Launcher { +class DelegatingLauncher implements Launcher { - protected T delegate; + protected Launcher delegate; - DelegatingLauncher(T delegate) { + DelegatingLauncher(Launcher delegate) { this.delegate = delegate; } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java deleted file mode 100644 index 20dbf2f6ed06..000000000000 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InterceptingClosableLauncher.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 static java.util.function.Function.identity; - -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; - -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 InterceptingClosableLauncher extends DelegatingCloseableLauncher { - - static CloseableLauncher decorate(Supplier launcherSupplier, List interceptors) { - Optional combinedInterceptor = composite(interceptors); - Launcher launcher = combinedInterceptor.map(it -> it.intercept(launcherSupplier::get)).orElseGet( - launcherSupplier); - return combinedInterceptor // - .map(it -> (CloseableLauncher) new InterceptingClosableLauncher(launcher, it)) // - .orElse(new DelegatingCloseableLauncher<>(launcher, identity())); - } - - private static Optional composite(List interceptors) { - if (interceptors.isEmpty()) { - return Optional.empty(); - } - return Optional.of(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)); - } - })); - } - - private final LauncherInterceptor interceptor; - - private InterceptingClosableLauncher(Launcher delegate, LauncherInterceptor interceptor) { - super(delegate, it -> { - interceptor.close(); - return it; - }); - 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/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 77edab9bbc67..eb60ce260404 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 @@ -100,8 +100,8 @@ public static LauncherSession openSession() throws PreconditionViolationExceptio public static LauncherSession openSession(LauncherConfig config) throws PreconditionViolationException { Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); - return new DefaultLauncherSession(() -> createDefaultLauncher(config, configurationParameters), - collectLauncherInterceptors(configurationParameters), createLauncherSessionListener(config)); + return new DefaultLauncherSession(collectLauncherInterceptors(configurationParameters), + () -> createLauncherSessionListener(config), () -> createDefaultLauncher(config, configurationParameters)); } /** @@ -131,7 +131,7 @@ public static Launcher create(LauncherConfig config) throws PreconditionViolatio Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); return new SessionPerRequestLauncher(() -> createDefaultLauncher(config, configurationParameters), - createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); + () -> createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); } private static DefaultLauncher createDefaultLauncher(LauncherConfig config, 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 1955bc625e56..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 @@ -29,13 +29,14 @@ class SessionPerRequestLauncher implements Launcher { private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry(); private final Supplier launcherSupplier; - private final LauncherSessionListener sessionListener; + private final Supplier sessionListenerSupplier; private final Supplier> interceptorFactory; - SessionPerRequestLauncher(Supplier launcherSupplier, LauncherSessionListener sessionListener, + SessionPerRequestLauncher(Supplier launcherSupplier, + Supplier sessionListenerSupplier, Supplier> interceptorFactory) { this.launcherSupplier = launcherSupplier; - this.sessionListener = sessionListener; + this.sessionListenerSupplier = sessionListenerSupplier; this.interceptorFactory = interceptorFactory; } @@ -71,8 +72,8 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { } private LauncherSession createSession() { - LauncherSession session = new DefaultLauncherSession(launcherSupplier, interceptorFactory.get(), - sessionListener); + LauncherSession session = new DefaultLauncherSession(interceptorFactory.get(), sessionListenerSupplier, + launcherSupplier); Launcher launcher = session.getLauncher(); listenerRegistry.launcherDiscoveryListeners.getListeners().forEach( launcher::registerLauncherDiscoveryListeners); 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..87cd780ba6f9 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java @@ -0,0 +1,35 @@ +/* + * 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/TestLauncherInterceptor1.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java index 585ccf74b6c4..6ff0dc7046dd 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor1.java @@ -17,13 +17,15 @@ 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(new URL[] { url }, originalClassLoader); + replacedClassLoader = new URLClassLoader(CLASSLOADER_NAME, new URL[] { url }, originalClassLoader); Thread.currentThread().setContextClassLoader(replacedClassLoader); } 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 index bdc198e3ee06..04f9ad3e29a0 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestLauncherInterceptor2.java @@ -12,9 +12,17 @@ public class TestLauncherInterceptor2 implements LauncherInterceptor { + public static boolean INTERCEPTING; + @Override public T intercept(Invocation invocation) { - return invocation.proceed(); + INTERCEPTING = true; + try { + return invocation.proceed(); + } + finally { + INTERCEPTING = false; + } } @Override 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 072c6119e369..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 @@ -37,6 +37,7 @@ import org.junit.platform.engine.UniqueId; 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; @@ -257,6 +258,7 @@ void createsLauncherInterceptorsBeforeDiscoveringTestEngines() { @Test void appliesLauncherInterceptorsToTestDiscovery() { + InterceptorInjectedLauncherSessionListener.CALLS = 0; withTestServices(() -> withSystemProperty(ENABLE_LAUNCHER_INTERCEPTORS, "true", () -> { var engine = new TestEngineSpy() { @Override @@ -277,11 +279,13 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId .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 @@ -310,6 +314,7 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult .hasRootCauseMessage("from execution") // .hasStackTraceContaining(TestLauncherInterceptor1.class.getName() + ".intercept(") // .hasStackTraceContaining(TestLauncherInterceptor2.class.getName() + ".intercept("); + assertThat(InterceptorInjectedLauncherSessionListener.CALLS).isEqualTo(1); })); } 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-tooling-support-tests/projects/standalone/expected-err.txt b/platform-tooling-support-tests/projects/standalone/expected-err.txt index 5788d5f3db68..cc4b8df06ddd 100644 --- a/platform-tooling-support-tests/projects/standalone/expected-err.txt +++ b/platform-tooling-support-tests/projects/standalone/expected-err.txt @@ -1,4 +1,6 @@ .+ 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: 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") // From 651c17c42ae5aa8790049274970ac9ce7d71a3c1 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 23 Jan 2023 09:37:10 +0100 Subject: [PATCH 5/7] Increase compatibility with GraalVM - Avoid early static initialization during native image compilation where possible - Use and document `--initialize-at-build-time` build argument --- .../release-notes/release-notes-5.10.0-M1.adoc | 3 ++- .../platform/launcher/core/EngineIdValidator.java | 9 ++++++--- .../platform/launcher/core/LauncherFactory.java | 12 +++++------- .../launcher/core/ServiceLoaderRegistry.java | 11 +++++++---- .../core/ServiceLoaderTestEngineRegistry.java | 9 ++++++--- .../InterceptorInjectedLauncherSessionListener.java | 9 +++++---- .../projects/graalvm-starter/build.gradle.kts | 9 +++++++++ 7 files changed, 40 insertions(+), 22 deletions(-) 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..716d9b3ce7e6 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 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/LauncherFactory.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java index eb60ce260404..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 @@ -66,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 */ } @@ -151,7 +149,7 @@ private static List collectLauncherInterceptors( LauncherConfigurationParameters configurationParameters) { if (configurationParameters.getBoolean(ENABLE_LAUNCHER_INTERCEPTORS).orElse(false)) { List interceptors = new ArrayList<>(); - SERVICE_LOADER_REGISTRY.load(LauncherInterceptor.class).forEach(interceptors::add); + ServiceLoaderRegistry.load(LauncherInterceptor.class).forEach(interceptors::add); return interceptors; } return emptyList(); @@ -169,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(); @@ -178,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; @@ -186,7 +184,7 @@ 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); @@ -203,7 +201,7 @@ private static void registerTestExecutionListeners(LauncherConfig config, Launch private static Stream loadAndFilterTestExecutionListeners( ConfigurationParameters configurationParameters) { - Iterable listeners = SERVICE_LOADER_REGISTRY.load(TestExecutionListener.class); + 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/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/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java index 87cd780ba6f9..bbaed89e2e91 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/InterceptorInjectedLauncherSessionListener.java @@ -28,8 +28,9 @@ public void launcherSessionOpened(LauncherSession session) { CALLS++; } - @Override - public void launcherSessionClosed(LauncherSession session) { - assertEquals(TestLauncherInterceptor1.CLASSLOADER_NAME, Thread.currentThread().getContextClassLoader().getName()); - } + @Override + public void launcherSessionClosed(LauncherSession session) { + assertEquals(TestLauncherInterceptor1.CLASSLOADER_NAME, + Thread.currentThread().getContextClassLoader().getName()); + } } 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") + } + } +} From f9a1d9ac9f3ed59920e492008d027e5fc59f69d5 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 31 Jan 2023 15:08:29 +0100 Subject: [PATCH 6/7] Document LauncherInterceptor --- .../src/docs/asciidoc/link-attributes.adoc | 1 + .../release-notes-5.10.0-M1.adoc | 5 ++ .../advanced-topics/launcher-api.adoc | 18 ++++++ .../asciidoc/user-guide/running-tests.adoc | 5 +- .../example/CustomLauncherInterceptor.java | 55 +++++++++++++++++++ .../platform/launcher/LauncherConstants.java | 11 ++++ .../launcher/LauncherInterceptor.java | 31 ++++++++++- 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 documentation/src/test/java/example/CustomLauncherInterceptor.java 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 716d9b3ce7e6..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 @@ -31,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-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java index 218bf3a077ed..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,16 @@ 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() { 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 index a8a4111dd327..7f4c0e17c70f 100644 --- 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 @@ -18,10 +18,37 @@ import org.apiguardian.api.API; /** - * Interceptor for test discovery and execution by a {@link Launcher}. + * 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 - * when the session is about to be closed. + * after the session is closed. They can + * {@linkplain #intercept(Invocation) intercept} the following invocations: + *

+ * + *

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 From 5f925cebbf85b4bed5ca1ea3953e55b91722af37 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 31 Jan 2023 15:49:28 +0100 Subject: [PATCH 7/7] Hide implementation details --- .../launcher/LauncherInterceptor.java | 39 ------------------ .../launcher/core/DefaultLauncherSession.java | 40 ++++++++++++++++++- 2 files changed, 38 insertions(+), 41 deletions(-) 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 index 7f4c0e17c70f..262a1d539809 100644 --- 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 @@ -11,9 +11,6 @@ package org.junit.platform.launcher; import static org.apiguardian.api.API.Status.EXPERIMENTAL; -import static org.apiguardian.api.API.Status.INTERNAL; - -import java.util.List; import org.apiguardian.api.API; @@ -58,18 +55,6 @@ @API(status = EXPERIMENTAL, since = "1.10") public interface LauncherInterceptor { - LauncherInterceptor NOOP = new LauncherInterceptor() { - @Override - public T intercept(Invocation invocation) { - return invocation.proceed(); - } - - @Override - public void close() { - // do nothing - } - }; - /** * Intercept the supplied invocation. * @@ -97,28 +82,4 @@ interface Invocation { T proceed(); } - @API(status = INTERNAL, since = "1.10") - static LauncherInterceptor composite(List interceptors) { - if (interceptors.isEmpty()) { - return NOOP; - } - 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/DefaultLauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java index 42969448616e..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 @@ -28,15 +28,27 @@ */ class DefaultLauncherSession implements LauncherSession { + 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(List interceptors, Supplier listenerSupplier, Supplier launcherSupplier) { - interceptor = LauncherInterceptor.composite(interceptors); + interceptor = composite(interceptors); Launcher launcher; - if (interceptor == LauncherInterceptor.NOOP) { + if (interceptor == NOOP_INTERCEPTOR) { this.listener = listenerSupplier.get(); launcher = launcherSupplier.get(); } @@ -98,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)); + } + }); + } }