diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index b00c5b1be896..5dc4b71657f6 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -21,6 +21,8 @@ endif::[] :ConsoleLauncher: {javadoc-root}/org/junit/platform/console/ConsoleLauncher.html[ConsoleLauncher] // :DiscoverySelectors_selectMethod: {javadoc-root}/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectMethod-java.lang.String-[selectMethod(String) in DiscoverySelectors] +:HierarchicalTestEngine: {javadoc-root}/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.html[HierarchicalTestEngine] +:ParallelExecutionConfigurationStrategy: {javadoc-root}/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.html[ParallelExecutionConfigurationStrategy] :TestEngine: {javadoc-root}/org/junit/platform/engine/TestEngine.html[TestEngine] // :Launcher: {javadoc-root}/org/junit/platform/launcher/Launcher.html[Launcher] @@ -54,6 +56,7 @@ endif::[] :EnabledIfSystemProperty: {javadoc-root}/org/junit/jupiter/api/condition/EnabledIfSystemProperty.html[@EnabledIfSystemProperty] :EnabledOnJre: {javadoc-root}/org/junit/jupiter/api/condition/EnabledOnJre.html[@EnabledOnJre] :EnabledOnOs: {javadoc-root}/org/junit/jupiter/api/condition/EnabledOnOs.html[@EnabledOnOs] +:Execution: {javadoc-root}/org/junit/jupiter/api/parallel/Execution.html[@Execution] :ExecutionCondition: {javadoc-root}/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition] :ExtendWith: {javadoc-root}/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith] :ExtensionContext: {javadoc-root}/org/junit/jupiter/api/extension/ExtensionContext.html[ExtensionContext] @@ -70,6 +73,7 @@ endif::[] :TestTemplate: {javadoc-root}/org/junit/jupiter/api/TestTemplate.html[@TestTemplate] :TestTemplateInvocationContext: {javadoc-root}/org/junit/jupiter/api/extension/TestTemplateInvocationContext.html[TestTemplateInvocationContext] :TestTemplateInvocationContextProvider: {javadoc-root}/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.html[TestTemplateInvocationContextProvider] +:ResourceLock: {javadoc-root}/org/junit/jupiter/api/parallel/ResourceLock.html[@ResourceLock] // :DisabledCondition: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DisabledCondition.java[DisabledCondition] :RepetitionInfoParameterResolver: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/RepetitionInfoParameterResolver.java[RepetitionInfoParameterResolver] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc index 34d72942700b..3396c778fc9a 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc @@ -3,7 +3,8 @@ *Date of Release:* ❓ -*Scope:* ❓ +*Scope:* Parallel test execution, output capturing, test sources for dynamic tests as well +as various minor improvements and bug fixes. For a complete list of all _closed_ issues and pull requests for this release, consult the link:{junit5-repo}+/milestone/23?closed=1+[5.3 M1] milestone page in the JUnit repository @@ -34,6 +35,19 @@ on GitHub. ==== New Features and Improvements +* Experimental support for capturing output printed to `System.out` and + `System.err` during test execution. This feature is disabled by default and can be + enabled using a configuration parameter (cf. + <<../user-guide/index.adoc#running-tests-capturing-output, User Guide>>). +* Reusable support for parallel test execution for test engines that extend + `HierarchicalTestEngine`. + - `HierarchicalTestEngine` implementations may now specify a + `HierarchicalTestExecutorService` + - By default, a `SameThreadHierarchicalTestExecutorService` is used. + - Test engines may use `ForkJoinPoolHierarchicalTestExecutorService` to support + parallel test execution based on the Fork/Join Framework. + - `Node` implementations may provide a set of `ExclusiveResources` and an + `ExecutionMode` to be used by `ForkJoinPoolHierarchicalTestExecutorService`. * New overloaded variant of `isAnnotated()` in `AnnotationSupport` that accepts `Optional` instead of `AnnotatedElement`. * New `--fail-if-no-tests` command-line option for the `ConsoleLauncher`. @@ -64,6 +78,10 @@ on GitHub. ==== New Features and Improvements +* Experimental support for parallel test execution. By default, tests are still executed + sequentially; parallelism can be enabled using a configuration parameter (please refer + to the <<../user-guide/index.adoc#writing-tests-parallel-execution, User Guide>> for + examples and configuration options). * New support for the IBM AIX operating system in `@EnabledOnOs` and `@DisabledOnOs`. * New `assertThrows` methods in `Assertions` provide a more specific failure message if the supplied lambda expression or method reference returns a result instead of throwing diff --git a/documentation/src/docs/asciidoc/user-guide/launcher-api.adoc b/documentation/src/docs/asciidoc/user-guide/launcher-api.adoc index 6c6a7dd3a349..1751c5e25585 100644 --- a/documentation/src/docs/asciidoc/user-guide/launcher-api.adoc +++ b/documentation/src/docs/asciidoc/user-guide/launcher-api.adoc @@ -78,6 +78,11 @@ is currently supported via Java's `java.util.ServiceLoader` mechanism. For examp in a file named `org.junit.platform.engine.TestEngine` within the `/META-INF/services` in the `junit-jupiter-engine` JAR. +NOTE: `{HierarchicalTestEngine}` is a convenient abstract base implementation (used by +the `{junit-jupiter-engine}`) that only requires implementors to provide the logic for +test discovery. It implements execution of `TestDescriptors` that implement the `Node` +interface, including support for parallel execution. + [[launcher-api-listeners-custom]] ==== Plugging in Your Own Test Execution Listeners diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index 39560938182c..070489b70b03 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -762,3 +762,28 @@ useful. | +(micro \| integration) & (foo \| baz)+ | all _micro_ or _integration_ tests for *foo* or *baz* |=== + +[[running-tests-capturing-output]] +=== Capturing Standard Output/Error + +Since version 1.3, the JUnit Platform provides opt-in support for capturing output +printed to `System.out` and `System.err`. To enable it, simply set the +`junit.platform.output.capture.stdout` and/or `junit.platform.output.capture.stderr` +<> to `true`. In addition, you may +configure the maximum number of buffered bytes to be used per executed test or container +using `junit.platform.output.capture.maxBuffer`. + +If enabled, the JUnit Platform captures the corresponding output and publishes it as a +report entry using the `stdout` or `stderr` keys to all registered +`{TestExecutionListener}` instances immediately before reporting the test or container as +finished. + +Please note that the captured output will only contain output emitted by the thread that +was used to execute a container or test. Any output by other threads will be omitted +because particularly when +<> it would be impossible +to attribute it to a specific test or container. + +WARNING: Capturing output is currently an _experimental_ feature. You're invited to give +it a try and provide feedback to the JUnit team so they can improve and eventually +<> this feature. diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 59e4407a8be3..be1a464a6e59 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1290,3 +1290,90 @@ The last method generates a nested hierarchy of dynamic tests utilizing ---- include::{testDir}/example/DynamicTestsDemo.java[tags=user_guide] ---- + + +[[writing-tests-parallel-execution]] +=== Parallel Execution + +By default, JUnit Jupiter tests are run sequentially in a single thread. Running tests in +parallel, e.g. to speed up execution, is available as an opt-in feature since version 5.3. +To enable parallel execution, simply set the set the +`junit.jupiter.execution.parallel.enabled` configuration parameter to `true`, e.g. in `junit-platform.properties` (see <> for other options). + +Once enabled, the JUnit Jupiter engine will execute tests on all levels of the test tree +fully in parallel according to the provided +<> while observing the declarative +<> mechanisms. Please +note that the <> feature needs to enabled separately. + +WARNING: Parallel test execution is currently an _experimental_ feature. You're invited +to give it a try and provide feedback to the JUnit team so they can improve and +eventually <> this feature. + +[[writing-tests-parallel-execution-config]] +==== Configuration + +Properties like the desired parallelism and the maximum pool size can be configured using +a `{ParallelExecutionConfigurationStrategy}`. The JUnit Platform provides two +implementations out of the box: `dynamic` and `fixed`. Alternatively, you may implement a +`custom` strategy. + +To select a strategy, simply set the `junit.jupiter.execution.parallel.config.strategy` +configuration parameter to one of the following options: + +`dynamic`:: + Computes the desired parallelism based on the number of available processors/cores + multiplied by the `junit.jupiter.execution.parallel.config.dynamic.factor` + configuration parameter (defaults to `1`). + +`fixed`:: + Uses the mandatory `junit.jupiter.execution.parallel.config.fixed.parallelism` + configuration parameter as desired parallelism. + +`custom`:: + Allows to specify a custom `{ParallelExecutionConfigurationStrategy}` implementation via + the mandatory `junit.jupiter.execution.parallel.config.custom.class` configuration + parameter to determine the desired configuration. + +If no configuration strategy is set, JUnit Jupiter uses the `dynamic` configuration +strategy with a factor of 1, i.e. the desired parallelism will equal the number of +available processors/cores. + +[[writing-tests-parallel-execution-synchronization]] +==== Synchronization + +In the `org.junit.jupiter.api.parallel` package, JUnit Jupiter provides two +annotation-based declarative mechanisms to change the execution mode and allow for +synchronization when using shared resources in different tests. + +If parallel execution is enabled, all classes and methods are executed concurrently by +default. You can change the execution mode for the annotated element and its subelements +(if any) by using the `{Execution}` annotation. The following two modes are available: + +`SAME_THREAD`:: + Force execution in the same thread used by the parent. For example, when used on a test + method, the test method will be executed in the same thread as any `@BeforeAll` or + `@AfterAll` methods of the containing test class. + +`CONCURRENT`:: + Execute concurrently unless a resource constraint forces execution in the same thread. + +In addition, the `{ResourceLock}` annotation allows to declare that a test class or +method uses a specific shared resource that requires synchronized access to ensure +reliable test execution. + +If the tests in the following example were run in parallel they would be flaky, i.e. +sometimes pass and other times fail, because of the inherent race condition of +writing and then reading the same system property. + +[source,java] +---- +include::{testDir}/example/SharedResourcesDemo.java[tags=user_guide] +---- + +When access to shared resources is declared using this annotation, the JUnit Jupiter +engine uses this information to ensure that no conflicting tests are run in parallel. + +In addition to the string that uniquely identifies the used resource, you may specify an +access mode. Two tests that require `READ` access to a resource may run in parallel with +each other but not while any other test that requires `READ_WRITE` access is running. diff --git a/documentation/src/test/java/example/SharedResourcesDemo.java b/documentation/src/test/java/example/SharedResourcesDemo.java new file mode 100644 index 000000000000..ac2da361acb6 --- /dev/null +++ b/documentation/src/test/java/example/SharedResourcesDemo.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; +import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ; +import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; +import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES; + +import java.util.Properties; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ResourceLock; + +// tag::user_guide[] +@Execution(CONCURRENT) +class SharedResourcesDemo { + + private Properties backup; + + @BeforeEach + void backup() { + backup = new Properties(); + backup.putAll(System.getProperties()); + } + + @AfterEach + void restore() { + System.setProperties(backup); + } + + @Test + @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ) + void customPropertyIsNotSetByDefault() { + assertNull(System.getProperty("my.prop")); + } + + @Test + @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) + void canSetCustomPropertyToFoo() { + System.setProperty("my.prop", "foo"); + assertEquals("foo", System.getProperty("my.prop")); + } + + @Test + @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) + void canSetCustomPropertyToBar() { + System.setProperty("my.prop", "bar"); + assertEquals("bar", System.getProperty("my.prop")); + } +} +// end::user_guide[] diff --git a/documentation/src/test/java/example/SlowTests.java b/documentation/src/test/java/example/SlowTests.java new file mode 100644 index 000000000000..67da3f110708 --- /dev/null +++ b/documentation/src/test/java/example/SlowTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package example; + +// tag::user_guide[] +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; + +@Disabled +class SlowTests { + + @Execution(SAME_THREAD) + @Test + void a() { + foo(); + } + + @Test + void b() { + foo(); + } + + @Test + void c() { + foo(); + } + + @Test + void d() { + foo(); + } + + @Test + void e() { + foo(); + } + + @Test + void f() { + foo(); + } + + @Test + void g() { + foo(); + } + + @Test + void h() { + foo(); + } + + @Test + void i() { + foo(); + } + + @Test + void j() { + foo(); + } + + @Test + void k() { + foo(); + } + + @Test + void l() { + foo(); + } + + @Test + void m() { + foo(); + } + + @Test + void n() { + foo(); + } + + @Test + void o() { + foo(); + } + + @Test + void p() { + foo(); + } + + @Execution(SAME_THREAD) + @Test + void q() { + foo(); + } + + @Test + void r() { + foo(); + } + + @Test + void s() { + foo(); + } + + private void foo() { + IntStream.range(1, 100_000_000).mapToDouble(i -> Math.pow(i, i)).map(Math::sqrt).max(); + } +} +// end::user_guide[] diff --git a/documentation/src/test/resources/junit-platform.properties b/documentation/src/test/resources/junit-platform.properties new file mode 100644 index 000000000000..585edf87a73f --- /dev/null +++ b/documentation/src/test/resources/junit-platform.properties @@ -0,0 +1,3 @@ +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.strategy=fixed +junit.jupiter.execution.parallel.config.fixed.parallelism=6 diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/Execution.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/Execution.java new file mode 100644 index 000000000000..ef5a953c290a --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/Execution.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.parallel; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @Execution} is used to configure the parallel execution mode of a test + * class or test method. + * + * @see Resources + * @see ResourceAccessMode + * @see ResourceLocks + * @since 5.3 + */ +@API(status = EXPERIMENTAL, since = "5.3") +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface Execution { + + /** + * The required/preferred execution mode. + * + * @see ExecutionMode + */ + ExecutionMode value(); + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ExecutionMode.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ExecutionMode.java new file mode 100644 index 000000000000..55fdd66d2fd7 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ExecutionMode.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.parallel; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * Supported execution modes for parallel execution. + * + * @since 5.3 + */ +@API(status = EXPERIMENTAL, since = "5.3") +public enum ExecutionMode { + + /** + * Force execution in same thread as the parent node. + */ + SAME_THREAD, + + /** + * Allow concurrent execution with any other node. + */ + CONCURRENT + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceAccessMode.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceAccessMode.java new file mode 100644 index 000000000000..2b33fd257c03 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceAccessMode.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.parallel; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * The access mode required by a test class or method for a given resource. + * + * @see ResourceLock + * @since 5.3 + */ +@API(status = EXPERIMENTAL, since = "5.3") +public enum ResourceAccessMode { + + /** + * Require read and write access to the resource. + */ + READ_WRITE, + + /** + * Require only read access to the resource. + */ + READ + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java new file mode 100644 index 000000000000..39210ecc0359 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.parallel; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @ResourceLock} is used to declare that the annotated test class or test + * method requires access to a resource identified by a key. + * + *

The resource key is specified using {@link #value()}. In addition, + * {@link #mode()} allows to specify whether the annotated test class or test + * method requires {@link ResourceAccessMode#READ_WRITE} or only + * {@link ResourceAccessMode#READ} access to the resource. In the former case, + * execution of the annotated element will occur while no other test class or + * test method that uses this resource is being executed. In the latter case, + * the annotated element may be executed concurrently with other test classes or + * methods that also require {@link ResourceAccessMode#READ} access but not at + * the same time as any other test that requires + * {@link ResourceAccessMode#READ_WRITE} access. + * + *

This annotation can be repeated to declare the use of multiple resources. + * + * @see Resources + * @see ResourceAccessMode + * @see ResourceLocks + * @since 5.3 + */ +@API(status = EXPERIMENTAL, since = "5.3") +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Repeatable(ResourceLocks.class) +public @interface ResourceLock { + + /** + * The resource key. + * + * @see Resources + */ + String value(); + + /** + * The resource access mode. + * + * @see ResourceAccessMode + */ + ResourceAccessMode mode() default ResourceAccessMode.READ_WRITE; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocks.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocks.java new file mode 100644 index 000000000000..34343d78b4e8 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocks.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.parallel; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @ResourceLocks} is a container for one or more + * {@link ResourceLock @ResourceLock} declarations. + * + *

Note, however, that use of the {@code @ResourceLocks} container is + * completely optional since {@code @ResourceLock} is a + * {@linkplain java.lang.annotation.Repeatable repeatable} annotation. + * + * @see ResourceLock + * @since 5.3 + */ +@API(status = EXPERIMENTAL, since = "5.3") +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface ResourceLocks { + + /** + * An array of one or more {@linkplain ResourceLock used resources}. + */ + ResourceLock[] value() default {}; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/Resources.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/Resources.java new file mode 100644 index 000000000000..589285dd35dd --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/Resources.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.parallel; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.io.PrintStream; +import java.util.Properties; + +import org.apiguardian.api.API; + +/** + * Common resource names for synchronizing test execution. + * + * @see ResourceLock + * @since 5.3 + */ +@API(status = EXPERIMENTAL, since = "5.3") +public class Resources { + + /** + * Represents Java's system properties. + * + * @see System#getProperties() + * @see System#setProperties(Properties) + */ + public static final String SYSTEM_PROPERTIES = "java.lang.System.properties"; + + /** + * Represents standard output stream of the current process. + * + * @see System#out + * @see System#setOut(PrintStream) + */ + public static final String SYSTEM_OUT = "java.lang.System.out"; + + /** + * Represents standard error stream of the current process. + * + * @see System#err + * @see System#setErr(PrintStream) + */ + public static final String SYSTEM_ERR = "java.lang.System.err"; + + private Resources() { + /* no-op */ + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/package-info.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/package-info.java new file mode 100644 index 000000000000..16b34edfb116 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/package-info.java @@ -0,0 +1,5 @@ +/** + * JUnit Jupiter API for influencing parallel test execution. + */ + +package org.junit.jupiter.api.parallel; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java index 5e20daf4f02e..2e212a5da371 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java @@ -10,13 +10,20 @@ package org.junit.jupiter.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_CUSTOM_CLASS_PROPERTY_NAME; +import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME; +import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_FIXED_PARALLELISM_PROPERTY_NAME; +import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_STRATEGY_PROPERTY_NAME; import org.apiguardian.api.API; +import org.junit.platform.engine.support.hierarchical.ParallelExecutionConfigurationStrategy; /** * Collection of constants related to the {@link JupiterTestEngine}. * + * @see org.junit.platform.engine.ConfigurationParameters * @since 5.0 */ @API(status = STABLE, since = "5.0") @@ -85,6 +92,67 @@ public final class Constants { */ public static final String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = "junit.jupiter.testinstance.lifecycle.default"; + /** + * Property name used to enable parallel test execution: {@value} + * + *

By default, tests are executed sequentially in a single thread. + * + * @since 5.3 + */ + @API(status = EXPERIMENTAL, since = "5.3") + public static final String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.parallel.enabled"; + + static final String PARALLEL_CONFIG_PREFIX = "junit.jupiter.execution.parallel.config."; + + /** + * Property name used to select the + * {@link ParallelExecutionConfigurationStrategy}: {@value} + * + *

Potential values: {@code dynamic} (default), {@code fixed}, or + * {@code custom}. + * + * @since 5.3 + */ + @API(status = EXPERIMENTAL, since = "5.3") + public static final String PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX + + CONFIG_STRATEGY_PROPERTY_NAME; + + /** + * Property name used to set the desired parallelism for the {@code fixed} + * configuration strategy: {@value} + * + *

No default value; must be an integer. + * + * @since 5.3 + */ + @API(status = EXPERIMENTAL, since = "5.3") + public static final String PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX + + CONFIG_FIXED_PARALLELISM_PROPERTY_NAME; + + /** + * Property name used to set the factor to be multiplied with the number of + * available processors/cores to determine the desired parallelism for the + * {@code dynamic} configuration strategy: {@value} + * + *

Value must be a decimal number; defaults to {@code 1}. + * + * @since 5.3 + */ + @API(status = EXPERIMENTAL, since = "5.3") + public static final String PARALLEL_CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX + + CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME; + + /** + * Property name used to specify the fully qualified class name of the + * {@link ParallelExecutionConfigurationStrategy} to be used for the + * {@code custom} configuration strategy: {@value} + * + * @since 5.3 + */ + @API(status = EXPERIMENTAL, since = "5.3") + public static final String PARALLEL_CONFIG_CUSTOM_CLASS_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX + + CONFIG_CUSTOM_CLASS_PROPERTY_NAME; + private Constants() { /* no-op */ } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java index c3cbb9c9d11c..151311ec44e7 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java @@ -11,6 +11,8 @@ package org.junit.jupiter.engine; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_PREFIX; +import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; import java.util.Optional; @@ -18,11 +20,15 @@ import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.config.PrefixedConfigurationParameters; +import org.junit.platform.engine.support.hierarchical.ForkJoinPoolHierarchicalTestExecutorService; import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine; +import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService; /** * The JUnit Jupiter {@link org.junit.platform.engine.TestEngine TestEngine}. @@ -62,6 +68,16 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId return engineDescriptor; } + @Override + protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest request) { + ConfigurationParameters config = request.getConfigurationParameters(); + if (config.getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME).orElse(false)) { + return new ForkJoinPoolHierarchicalTestExecutorService( + new PrefixedConfigurationParameters(config, PARALLEL_CONFIG_PREFIX)); + } + return super.createExecutorService(request); + } + @Override protected JupiterEngineExecutionContext createExecutionContext(ExecutionRequest request) { return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java index a591d24b2339..2a97d78ff6b4 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java @@ -50,6 +50,7 @@ import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; /** * {@link TestDescriptor} for tests based on Java classes. @@ -117,6 +118,16 @@ private static String generateDefaultDisplayName(Class testClass) { // --- Node ---------------------------------------------------------------- + @Override + public ExecutionMode getExecutionMode() { + return getExecutionMode(getTestClass()); + } + + @Override + public Set getExclusiveResources() { + return getExclusiveResources(getTestClass()); + } + @Override public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { ExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation( diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java index a0764082fe0e..d7d7622d43fb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java @@ -10,6 +10,8 @@ package org.junit.jupiter.engine.descriptor; +import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.CONCURRENT; + import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.platform.engine.TestDescriptor; @@ -30,6 +32,11 @@ abstract class DynamicNodeTestDescriptor extends JupiterTestDescriptor { this.index = index; } + @Override + public ExecutionMode getExecutionMode() { + return getParent().map(parent -> ((JupiterTestDescriptor) parent).getExecutionMode()).orElse(CONCURRENT); + } + @Override public String getLegacyReportingName() { // @formatter:off diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java index 0c22a4f02a14..9e94731d8896 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java @@ -12,6 +12,7 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toSet; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; @@ -28,8 +29,12 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.engine.execution.ConditionEvaluator; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.ExceptionUtils; @@ -38,6 +43,8 @@ import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode; import org.junit.platform.engine.support.hierarchical.Node; /** @@ -103,6 +110,46 @@ protected static String determineDisplayName(E elem // --- Node ---------------------------------------------------------------- + protected ExecutionMode getExecutionMode(AnnotatedElement element) { + // @formatter:off + return findAnnotation(element, Execution.class) + .map(Execution::value) + .map(JupiterTestDescriptor::toExecutionMode) + .orElseGet(() -> getParent() + .filter(parent -> parent instanceof Node) + .map(parent -> ((Node) parent).getExecutionMode()) + .orElse(ExecutionMode.CONCURRENT)); + // @formatter:on + } + + private static ExecutionMode toExecutionMode(org.junit.jupiter.api.parallel.ExecutionMode mode) { + switch (mode) { + case CONCURRENT: + return ExecutionMode.CONCURRENT; + case SAME_THREAD: + return ExecutionMode.SAME_THREAD; + } + throw new JUnitException("Unknown ExecutionMode: " + mode); + } + + protected Set getExclusiveResources(AnnotatedElement element) { + // @formatter:off + return findRepeatableAnnotations(element, ResourceLock.class).stream() + .map(resource -> new ExclusiveResource(resource.value(), toLockMode(resource.mode()))) + .collect(toSet()); + // @formatter:on + } + + private static LockMode toLockMode(ResourceAccessMode mode) { + switch (mode) { + case READ: + return LockMode.READ; + case READ_WRITE: + return LockMode.READ_WRITE; + } + throw new JUnitException("Unknown ResourceAccessMode: " + mode); + } + @Override public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) throws Exception { ConditionEvaluationResult evaluationResult = conditionEvaluator.evaluate(context.getExtensionRegistry(), diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java index c17d545e0b27..ca84f0106c5b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java @@ -20,6 +20,7 @@ import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; /** * Base class for {@link TestDescriptor TestDescriptors} based on Java methods. @@ -56,6 +57,16 @@ public final Set getTags() { return allTags; } + @Override + public Set getExclusiveResources() { + return getExclusiveResources(getTestMethod()); + } + + @Override + public ExecutionMode getExecutionMode() { + return getExecutionMode(getTestMethod()); + } + public final Class getTestClass() { return this.testClass; } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/logging/LogRecordListener.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/logging/LogRecordListener.java index 3c9957c24e14..09745d77d1c7 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/logging/LogRecordListener.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/logging/LogRecordListener.java @@ -30,19 +30,21 @@ @API(status = INTERNAL, since = "1.1") public class LogRecordListener { - private final List logRecords = new ArrayList<>(); + // capture log records by thread to support parallel test execution + private final ThreadLocal> logRecords = ThreadLocal.withInitial(ArrayList::new); /** * Inform the listener of a {@link LogRecord} that was submitted to JUL for * processing. */ public void logRecordSubmitted(LogRecord logRecord) { - this.logRecords.add(logRecord); + this.logRecords.get().add(logRecord); } /** * Get a stream of {@link LogRecord log records} that have been - * {@linkplain #logRecordSubmitted submitted} to this listener. + * {@linkplain #logRecordSubmitted submitted} to this listener by the + * current thread. * *

As stated in the JavaDoc for {@code LogRecord}, a submitted * {@code LogRecord} should not be updated by the client application. Thus, @@ -50,13 +52,13 @@ public void logRecordSubmitted(LogRecord logRecord) { * testing purposes and not modified in any way. */ public Stream stream() { - return this.logRecords.stream(); + return this.logRecords.get().stream(); } /** * Get a stream of {@link LogRecord log records} that have been * {@linkplain #logRecordSubmitted submitted} to this listener for the - * logger name equal to the name of the given class. + * logger name equal to the name of the given class by the current thread. * *

As stated in the JavaDoc for {@code LogRecord}, a submitted * {@code LogRecord} should not be updated by the client application. Thus, @@ -78,7 +80,8 @@ public Stream stream(Class clazz) { /** * Get a stream of {@link LogRecord log records} that have been * {@linkplain #logRecordSubmitted submitted} to this listener for the - * logger name equal to the name of the given class at the given log level. + * logger name equal to the name of the given class at the given log level + * by the current thread. * *

As stated in the JavaDoc for {@code LogRecord}, a submitted * {@code LogRecord} should not be updated by the client application. Thus, @@ -100,10 +103,11 @@ public Stream stream(Class clazz, Level level) { /** * Clear all existing {@link LogRecord log records} that have been - * {@linkplain #logRecordSubmitted submitted} to this listener. + * {@linkplain #logRecordSubmitted submitted} to this listener by the + * current thread. */ public void clear() { - this.logRecords.clear(); + this.logRecords.get().clear(); } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java index 7fa0f46fcda6..3f5aac4aecec 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java @@ -105,7 +105,8 @@ private String colorCaption(TreeNode node) { if (node.reason().isPresent()) { return color(SKIPPED, caption); } - return color(Color.valueOf(node.identifier().orElseThrow(AssertionError::new)), caption); + Color color = node.identifier().map(Color::valueOf).orElse(Color.NONE); + return color(color, caption); } private void printThrowable(String indent, TestExecutionResult result) { diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java index 8678834eae85..fc60307b4e27 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java @@ -11,8 +11,9 @@ package org.junit.platform.console.tasks; import java.io.PrintWriter; -import java.util.ArrayDeque; -import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; import org.junit.platform.console.options.Theme; import org.junit.platform.engine.TestExecutionResult; @@ -26,44 +27,53 @@ */ class TreePrintingListener implements TestExecutionListener { - private final Deque stack; + private final Map nodesByUniqueId = new ConcurrentHashMap<>(); + private TreeNode root; private final TreePrinter treePrinter; TreePrintingListener(PrintWriter out, boolean disableAnsiColors, Theme theme) { this.treePrinter = new TreePrinter(out, theme, disableAnsiColors); - this.stack = new ArrayDeque<>(); + } + + private TreeNode addNode(TestIdentifier testIdentifier, Supplier nodeSupplier) { + TreeNode node = nodeSupplier.get(); + nodesByUniqueId.put(testIdentifier.getUniqueId(), node); + testIdentifier.getParentId().map(nodesByUniqueId::get).orElse(root).addChild(node); + return node; + } + + private TreeNode getNode(TestIdentifier testIdentifier) { + return nodesByUniqueId.get(testIdentifier.getUniqueId()); } @Override public void testPlanExecutionStarted(TestPlan testPlan) { - stack.push(new TreeNode(testPlan.toString())); + root = new TreeNode(testPlan.toString()); } @Override public void testPlanExecutionFinished(TestPlan testPlan) { - treePrinter.print(stack.pop()); + treePrinter.print(root); } @Override public void executionStarted(TestIdentifier testIdentifier) { - TreeNode node = new TreeNode(testIdentifier); - stack.peek().addChild(node); - stack.push(node); + addNode(testIdentifier, () -> new TreeNode(testIdentifier)); } @Override public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { - stack.pop().setResult(testExecutionResult); + getNode(testIdentifier).setResult(testExecutionResult); } @Override public void executionSkipped(TestIdentifier testIdentifier, String reason) { - stack.peek().addChild(new TreeNode(testIdentifier, reason)); + addNode(testIdentifier, () -> new TreeNode(testIdentifier, reason)); } @Override public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) { - stack.peek().addReportEntry(entry); + getNode(testIdentifier).addReportEntry(entry); } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/ConfigurationParameters.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/ConfigurationParameters.java index 13cfad22fc3e..3d65fad7b7a6 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/ConfigurationParameters.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/ConfigurationParameters.java @@ -13,8 +13,11 @@ import static org.apiguardian.api.API.Status.STABLE; import java.util.Optional; +import java.util.function.Function; import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; /** * Configuration parameters that {@link TestEngine TestEngines} may use to @@ -83,6 +86,45 @@ public interface ConfigurationParameters { */ Optional getBoolean(String key); + /** + * Get and transform the configuration parameter stored under the specified + * {@code key} using the specified {@code transformer}. + * + *

If no such key is present in this {@code ConfigurationParameters}, + * an attempt will be made to look up the value as a JVM system property. + * If no such system property exists, an attempt will be made to look up + * the value in the {@linkplain #CONFIG_FILE_NAME JUnit Platform properties + * file}. + * + *

In case the transformer throws an exception, it will be wrapped in a + * {@link JUnitException} with a helpful message. + * + * @param key the key to look up; never {@code null} or blank + * @param transformer the transformer to apply in case a value is found; + * never {@code null} + * @return an {@code Optional} containing the value; never {@code null} + * but potentially empty + * + * @see #getBoolean(String) + * @see System#getProperty(String) + * @see #CONFIG_FILE_NAME + * @since 1.3 + */ + @API(status = STABLE, since = "1.3") + default Optional get(String key, Function transformer) { + Preconditions.notNull(transformer, "transformer must not be null"); + return get(key).map(input -> { + try { + return transformer.apply(input); + } + catch (Exception ex) { + String message = String.format( + "Failed to transform configuration parameter with key '%s' and initial value '%s'", key, input); + throw new JUnitException(message, ex); + } + }); + } + /** * Get the number of configuration parameters stored directly in this * {@code ConfigurationParameters}. diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/config/PrefixedConfigurationParameters.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/config/PrefixedConfigurationParameters.java new file mode 100644 index 000000000000..fff9312b3463 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/config/PrefixedConfigurationParameters.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.config; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.Optional; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.ConfigurationParameters; + +/** + * View of {@link ConfigurationParameters} that applies a supplied prefix to all + * queries. + * + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public class PrefixedConfigurationParameters implements ConfigurationParameters { + + private final ConfigurationParameters delegate; + private final String prefix; + + /** + * Create a new view of the supplied {@link ConfigurationParameters} that + * applies the supplied prefix to all queries. + * + * @param delegate the {@link ConfigurationParameters} to delegate to; never + * {@code null} + * @param prefix the prefix to apply to all queries; never {@code null} + */ + public PrefixedConfigurationParameters(ConfigurationParameters delegate, String prefix) { + this.delegate = Preconditions.notNull(delegate, "delegate must not be null"); + this.prefix = Preconditions.notNull(prefix, "prefix must not be null"); + } + + @Override + public Optional get(String key) { + return delegate.get(prefixed(key)); + } + + @Override + public Optional getBoolean(String key) { + return delegate.getBoolean(prefixed(key)); + } + + @Override + public Optional get(String key, Function transformer) { + return delegate.get(prefixed(key), transformer); + } + + private String prefixed(String key) { + return prefix + key; + } + + @Override + public int size() { + return delegate.size(); + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/config/package-info.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/config/package-info.java new file mode 100644 index 000000000000..05f555a220ea --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/config/package-info.java @@ -0,0 +1,6 @@ +/** + * {@link org.junit.platform.engine.ConfigurationParameters}-related support + * classes intended to be used by test engine implementations. + */ + +package org.junit.platform.engine.support.config; diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/CompositeLock.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/CompositeLock.java new file mode 100644 index 000000000000..8b5d10278398 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/CompositeLock.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.locks.Lock; + +/** + * @since 1.3 + */ +class CompositeLock implements ResourceLock { + + private final List locks; + + CompositeLock(List locks) { + this.locks = locks; + } + + // for tests only + List getLocks() { + return locks; + } + + @Override + public ResourceLock acquire() throws InterruptedException { + ForkJoinPool.managedBlock(new CompositeLockManagedBlocker()); + return this; + } + + private void acquireAllLocks() throws InterruptedException { + List acquiredLocks = new ArrayList<>(locks.size()); + try { + for (Lock lock : locks) { + lock.lockInterruptibly(); + acquiredLocks.add(lock); + } + } + catch (InterruptedException e) { + release(acquiredLocks); + throw e; + } + } + + @Override + public void release() { + release(locks); + } + + private void release(List acquiredLocks) { + for (int i = acquiredLocks.size() - 1; i >= 0; i--) { + acquiredLocks.get(i).unlock(); + } + } + + private class CompositeLockManagedBlocker implements ForkJoinPool.ManagedBlocker { + private boolean acquired; + + @Override + public boolean block() throws InterruptedException { + acquireAllLocks(); + acquired = true; + return true; + } + + @Override + public boolean isReleasable() { + return acquired; + } + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfiguration.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfiguration.java new file mode 100644 index 000000000000..26e5706ef0a9 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +/** + * @since 1.3 + */ +class DefaultParallelExecutionConfiguration implements ParallelExecutionConfiguration { + + private final int parallelism; + private final int minimumRunnable; + private final int maxPoolSize; + private final int corePoolSize; + private final int keepAlive; + + DefaultParallelExecutionConfiguration(int parallelism, int minimumRunnable, int maxPoolSize, int corePoolSize, + int keepAlive) { + this.parallelism = parallelism; + this.minimumRunnable = minimumRunnable; + this.maxPoolSize = maxPoolSize; + this.corePoolSize = corePoolSize; + this.keepAlive = keepAlive; + } + + @Override + public int getParallelism() { + return parallelism; + } + + @Override + public int getMinimumRunnable() { + return minimumRunnable; + } + + @Override + public int getMaxPoolSize() { + return maxPoolSize; + } + + @Override + public int getCorePoolSize() { + return corePoolSize; + } + + @Override + public int getKeepAlive() { + return keepAlive; + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java new file mode 100644 index 000000000000..4e5cc3a332f7 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java @@ -0,0 +1,129 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.math.BigDecimal; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.ConfigurationParameters; + +/** + * Default implementations of configuration strategies for parallel test + * execution. + * + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public enum DefaultParallelExecutionConfigurationStrategy implements ParallelExecutionConfigurationStrategy { + + /** + * Uses the mandatory {@value CONFIG_FIXED_PARALLELISM_PROPERTY_NAME} configuration + * parameter as desired parallelism. + */ + FIXED { + @Override + public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) { + int parallelism = configurationParameters.get(CONFIG_FIXED_PARALLELISM_PROPERTY_NAME, + Integer::valueOf).orElseThrow( + () -> new JUnitException(CONFIG_FIXED_PARALLELISM_PROPERTY_NAME + " must be set")); + return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism, + 30); + } + }, + + /** + * Computes the desired parallelism based on the number of available + * processors/cores multiplied by the {@value CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME} + * configuration parameter. + */ + DYNAMIC { + @Override + public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) { + BigDecimal factor = configurationParameters.get(CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME, + BigDecimal::new).orElse(BigDecimal.ONE); + Preconditions.condition(factor.compareTo(BigDecimal.ZERO) > 0, + () -> CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME + " must be greater than 0"); + int parallelism = Math.max(1, + factor.multiply(BigDecimal.valueOf(Runtime.getRuntime().availableProcessors())).intValue()); + return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism, + 30); + } + }, + + /** + * Allows to specify a custom {@link ParallelExecutionConfigurationStrategy} + * implementation via the mandatory {@value CONFIG_CUSTOM_CLASS_PROPERTY_NAME} + * configuration parameter to determine the desired configuration. + */ + CUSTOM { + @Override + public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) { + String className = configurationParameters.get(CONFIG_CUSTOM_CLASS_PROPERTY_NAME).orElseThrow( + () -> new JUnitException(CONFIG_CUSTOM_CLASS_PROPERTY_NAME + " must be set")); + Class strategyClass = ReflectionUtils.loadClass(className).orElseThrow( + () -> new JUnitException("Could not load class for " + CONFIG_CUSTOM_CLASS_PROPERTY_NAME)); + Preconditions.condition(ParallelExecutionConfigurationStrategy.class.isAssignableFrom(strategyClass), + CONFIG_CUSTOM_CLASS_PROPERTY_NAME + " does not implement " + + ParallelExecutionConfigurationStrategy.class); + ParallelExecutionConfigurationStrategy strategy = (ParallelExecutionConfigurationStrategy) ReflectionUtils.newInstance( + strategyClass); + return strategy.createConfiguration(configurationParameters); + } + }; + + /** + * Property name used to determine the desired configuration strategy. + * + *

Value must be one of {@code dynamic}, {@code fixed}, or + * {@code custom}. + */ + public static final String CONFIG_STRATEGY_PROPERTY_NAME = "strategy"; + + /** + * Property name used to determine the desired parallelism by the + * {@link #FIXED} configuration strategy. + * + *

No default value; must be an integer. + * + * @see #FIXED + */ + public static final String CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = "fixed.parallelism"; + + /** + * Property name of the factor used to determine the desired parallelism by the + * {@link #DYNAMIC} configuration strategy. + * + *

Value must be a decimal number; defaults to {@code 1}. + * + * @see #DYNAMIC + */ + public static final String CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = "dynamic.factor"; + + /** + * Property name used to specify the fully qualified class name of the + * {@link ParallelExecutionConfigurationStrategy} to be used by the + * {@link #CUSTOM} configuration strategy. + * + * @see #CUSTOM + */ + @API(status = EXPERIMENTAL, since = "5.3") + public static final String CONFIG_CUSTOM_CLASS_PROPERTY_NAME = "custom.class"; + + static ParallelExecutionConfigurationStrategy getStrategy(ConfigurationParameters configurationParameters) { + return valueOf(configurationParameters.get(CONFIG_STRATEGY_PROPERTY_NAME).orElse("dynamic").toUpperCase()); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java new file mode 100644 index 000000000000..2a686e29fe4d --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java @@ -0,0 +1,112 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.Objects; +import java.util.concurrent.locks.ReadWriteLock; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ToStringBuilder; + +/** + * An exclusive resource identified by a key with a lock mode that is used to + * synchronize access to shared resources when executing nodes in parallel. + * + * @see Node#getExecutionMode() + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public class ExclusiveResource { + + private final String key; + private final LockMode lockMode; + private int hash; + + /** + * Create a new {@code ExclusiveResource}. + * + * @param key the identifier of the resource; never {@code null} or blank + * @param lockMode the lock mode to use to synchronize access to the + * resource; never {@code null} + */ + public ExclusiveResource(String key, LockMode lockMode) { + this.key = Preconditions.notBlank(key, "key must not be blank"); + this.lockMode = Preconditions.notNull(lockMode, "lockMode must not be null"); + } + + /** + * Get the key of this resource. + */ + public String getKey() { + return key; + } + + /** + * Get the lock mode of this resource. + */ + public LockMode getLockMode() { + return lockMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExclusiveResource that = (ExclusiveResource) o; + return Objects.equals(key, that.key) && lockMode == that.lockMode; + } + + @Override + public int hashCode() { + int h = hash; + if (h == 0) { + h = hash = Objects.hash(key, lockMode); + } + return h; + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("key", key).append("lockMode", lockMode).toString(); + } + + /** + * {@code LockMode} translates to the respective {@link ReadWriteLock} + * locks. + * + * @implNote Enum order is important, since it can be used to sort locks, so + * the stronger mode has to be first. + */ + public enum LockMode { + + /** + * Require read and write access to the resource. + * + * @see ReadWriteLock#writeLock() + */ + READ_WRITE, + + /** + * Require only read access to the resource. + * + * @see ReadWriteLock#readLock() + */ + READ + + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExecutionTracker.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExecutionTracker.java deleted file mode 100644 index b9a9d55ea2a4..000000000000 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExecutionTracker.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2015-2018 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 - * - * http://www.eclipse.org/legal/epl-v20.html - */ - -package org.junit.platform.engine.support.hierarchical; - -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; - -/** - * @since 1.0 - */ -class ExecutionTracker { - - private final Set executedUniqueIds = ConcurrentHashMap.newKeySet(); - - void markExecuted(TestDescriptor testDescriptor) { - executedUniqueIds.add(testDescriptor.getUniqueId()); - } - - boolean wasAlreadyExecuted(TestDescriptor testDescriptor) { - return executedUniqueIds.contains(testDescriptor.getUniqueId()); - } -} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java new file mode 100644 index 000000000000..2ae0c09c509c --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java @@ -0,0 +1,168 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.ForkJoinPool.defaultForkJoinWorkerThreadFactory; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.CONCURRENT; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.reflect.Constructor; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.Future; +import java.util.concurrent.RecursiveAction; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import org.apiguardian.api.API; +import org.junit.platform.commons.logging.LoggerFactory; +import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.engine.ConfigurationParameters; + +/** + * A {@link ForkJoinPool}-based + * {@linkplain HierarchicalTestExecutorService executor service} that executes + * {@linkplain TestTask test tasks} with a configured parallelism. + * + * @see ForkJoinPool + * @see DefaultParallelExecutionConfigurationStrategy + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public class ForkJoinPoolHierarchicalTestExecutorService implements HierarchicalTestExecutorService { + + private final ForkJoinPool forkJoinPool; + + /** + * Create a new {@code ForkJoinPoolHierarchicalTestExecutorService} based on + * the supplied {@link ConfigurationParameters}. + * + * @see DefaultParallelExecutionConfigurationStrategy + */ + public ForkJoinPoolHierarchicalTestExecutorService(ConfigurationParameters configurationParameters) { + forkJoinPool = createForkJoinPool(configurationParameters); + LoggerFactory.getLogger(ForkJoinPoolHierarchicalTestExecutorService.class) // + .config(() -> "Using ForkJoinPool with parallelism of " + forkJoinPool.getParallelism()); + } + + private ForkJoinPool createForkJoinPool(ConfigurationParameters configurationParameters) { + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.getStrategy( + configurationParameters); + ParallelExecutionConfiguration configuration = strategy.createConfiguration(configurationParameters); + try { + // Try to use constructor available in Java >= 9 + Constructor constructor = ForkJoinPool.class.getDeclaredConstructor(Integer.TYPE, + ForkJoinWorkerThreadFactory.class, UncaughtExceptionHandler.class, Boolean.TYPE, Integer.TYPE, + Integer.TYPE, Integer.TYPE, Predicate.class, Long.TYPE, TimeUnit.class); + return constructor.newInstance(configuration.getParallelism(), defaultForkJoinWorkerThreadFactory, null, + false, configuration.getCorePoolSize(), configuration.getMaxPoolSize(), + configuration.getMinimumRunnable(), null, configuration.getKeepAlive(), TimeUnit.SECONDS); + } + catch (Exception e) { + // Fallback for Java 8 + return new ForkJoinPool(configuration.getParallelism()); + } + } + + @Override + public Future submit(TestTask testTask) { + ExclusiveTask exclusiveTask = new ExclusiveTask(testTask); + if (!isAlreadyRunningInForkJoinPool()) { + // ensure we're running inside the ForkJoinPool so we + // can use ForkJoinTask API in invokeAll etc. + return forkJoinPool.submit(exclusiveTask); + } + if (testTask.getExecutionMode() == CONCURRENT) { + return exclusiveTask.fork(); + } + exclusiveTask.compute(); + return completedFuture(null); + } + + private boolean isAlreadyRunningInForkJoinPool() { + Thread currentThread = Thread.currentThread(); + return currentThread instanceof ForkJoinWorkerThread + && ((ForkJoinWorkerThread) currentThread).getPool() == forkJoinPool; + } + + @Override + public void invokeAll(List tasks) { + if (tasks.size() == 1) { + new ExclusiveTask(tasks.get(0)).compute(); + return; + } + Deque nonConcurrentTasks = new LinkedList<>(); + Deque concurrentTasksInReverseOrder = new LinkedList<>(); + forkConcurrentTasks(tasks, nonConcurrentTasks, concurrentTasksInReverseOrder); + executeNonConcurrentTasks(nonConcurrentTasks); + joinConcurrentTasksInReverseOrderToEnableWorkStealing(concurrentTasksInReverseOrder); + } + + private void forkConcurrentTasks(List tasks, Deque nonConcurrentTasks, + Deque concurrentTasksInReverseOrder) { + for (TestTask testTask : tasks) { + ExclusiveTask exclusiveTask = new ExclusiveTask(testTask); + if (testTask.getExecutionMode() == CONCURRENT) { + exclusiveTask.fork(); + concurrentTasksInReverseOrder.addFirst(exclusiveTask); + } + else { + nonConcurrentTasks.add(exclusiveTask); + } + } + } + + private void executeNonConcurrentTasks(Deque nonConcurrentTasks) { + for (ExclusiveTask task : nonConcurrentTasks) { + task.compute(); + } + } + + private void joinConcurrentTasksInReverseOrderToEnableWorkStealing( + Deque concurrentTasksInReverseOrder) { + for (ExclusiveTask forkedTask : concurrentTasksInReverseOrder) { + forkedTask.join(); + } + } + + @Override + public void close() { + forkJoinPool.shutdownNow(); + } + + // this class cannot not be serialized because TestTask is not Serializable + @SuppressWarnings("serial") + static class ExclusiveTask extends RecursiveAction { + + private final TestTask testTask; + + ExclusiveTask(TestTask testTask) { + this.testTask = testTask; + } + + @SuppressWarnings("try") + @Override + public void compute() { + try (ResourceLock lock = testTask.getResourceLock().acquire()) { + testTask.execute(); + } + catch (InterruptedException e) { + ExceptionUtils.throwAsUncheckedException(e); + } + } + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.java index f926846d9a2c..240a2d3a770e 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.java @@ -10,9 +10,11 @@ package org.junit.platform.engine.support.hierarchical; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestEngine; @@ -36,16 +38,44 @@ public abstract class HierarchicalTestEngine i * listener} of test execution events. * * @see Node + * @see #createExecutorService * @see #createExecutionContext */ @Override public final void execute(ExecutionRequest request) { - new HierarchicalTestExecutor<>(request, createExecutionContext(request)).execute(); + try (HierarchicalTestExecutorService executorService = createExecutorService(request)) { + new HierarchicalTestExecutor<>(request, createExecutionContext(request), executorService).execute().get(); + } + catch (Exception exception) { + throw new JUnitException("Error executing tests for engine " + getId(), exception); + } + } + + /** + * Create the {@linkplain HierarchicalTestExecutorService executor service} + * to use for executing the supplied {@linkplain ExecutionRequest request}. + * + *

An engine may use the information in the supplied request + * such as the contained + * {@linkplain ExecutionRequest#getConfigurationParameters() configuration parameters} + * to decide what kind of service to return or how to configure it. + * + *

By default, this method returns an instance of + * {@link SameThreadHierarchicalTestExecutorService}. + * + * @param request the request about to be executed + * @see ForkJoinPoolHierarchicalTestExecutorService + * @see SameThreadHierarchicalTestExecutorService + * @since 1.3 + */ + @API(status = EXPERIMENTAL, since = "1.3") + protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest request) { + return new SameThreadHierarchicalTestExecutorService(); } /** * Create the initial execution context for executing the supplied - * {@link ExecutionRequest request}. + * {@linkplain ExecutionRequest request}. * * @param request the request about to be executed * @return the initial context that will be passed to nodes in the hierarchy diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java index f618e35fb608..3b2aefcc6c4a 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java @@ -10,18 +10,12 @@ package org.junit.platform.engine.support.hierarchical; -import static org.junit.platform.commons.util.BlacklistedExceptions.rethrowIfBlacklisted; -import static org.junit.platform.engine.TestExecutionResult.Status.FAILED; - -import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.Future; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.support.hierarchical.Node.SkipResult; /** * Implementation core of all {@link TestEngine TestEngines} that wish to @@ -39,170 +33,27 @@ */ class HierarchicalTestExecutor { - private static final SingleTestExecutor singleTestExecutor = new SingleTestExecutor(); - - private final TestDescriptor rootTestDescriptor; - private final EngineExecutionListener listener; + private final ExecutionRequest request; private final C rootContext; + private final HierarchicalTestExecutorService executorService; - HierarchicalTestExecutor(ExecutionRequest request, C rootContext) { - this.rootTestDescriptor = request.getRootTestDescriptor(); - this.listener = request.getEngineExecutionListener(); + HierarchicalTestExecutor(ExecutionRequest request, C rootContext, HierarchicalTestExecutorService executorService) { + this.request = request; this.rootContext = rootContext; + this.executorService = executorService; } - void execute() { - new NodeExecutor(this.rootTestDescriptor).execute(this.rootContext, new ExecutionTracker()); + Future execute() { + NodeTestTask rootTestTask = prepareNodeTestTaskTree(); + rootTestTask.setParentContext(this.rootContext); + return this.executorService.submit(rootTestTask); } - class NodeExecutor { - - private final TestDescriptor testDescriptor; - private final Node node; - private final List executionErrors = new ArrayList<>(); - private C context; - private SkipResult skipResult; - private TestExecutionResult executionResult; - - NodeExecutor(TestDescriptor testDescriptor) { - this.testDescriptor = testDescriptor; - node = asNode(testDescriptor); - } - - void execute(C parentContext, ExecutionTracker tracker) { - tracker.markExecuted(testDescriptor); - prepare(parentContext); - if (executionErrors.isEmpty()) { - checkWhetherSkipped(); - } - if (executionErrors.isEmpty() && !skipResult.isSkipped()) { - executeRecursively(tracker); - } - if (context != null) { - cleanUp(); - } - reportDone(); - } - - private void prepare(C parentContext) { - try { - context = node.prepare(parentContext); - } - catch (Throwable t) { - addExecutionError(t); - } - } - - private void checkWhetherSkipped() { - try { - skipResult = node.shouldBeSkipped(context); - } - catch (Throwable t) { - addExecutionError(t); - } - } - - private void executeRecursively(ExecutionTracker tracker) { - listener.executionStarted(testDescriptor); - - executionResult = singleTestExecutor.executeSafely(() -> { - Throwable failure = null; - try { - context = node.before(context); - - context = node.execute(context, dynamicTestDescriptor -> { - listener.dynamicTestRegistered(dynamicTestDescriptor); - new NodeExecutor(dynamicTestDescriptor).execute(context, tracker); - }); - - // @formatter:off - testDescriptor.getChildren().stream() - .filter(child -> !tracker.wasAlreadyExecuted(child)) - .forEach(child -> new NodeExecutor(child).execute(context, tracker)); - // @formatter:on - } - catch (Throwable t) { - failure = t; - } - finally { - executeAfter(failure); - } - }); - } - - private void executeAfter(Throwable failure) throws Throwable { - try { - node.after(context); - } - catch (Throwable t) { - if (failure != null && failure != t) { - failure.addSuppressed(t); - } - else { - throw t; - } - } - if (failure != null) { - throw failure; - } - } - - private void cleanUp() { - try { - node.cleanUp(context); - } - catch (Throwable t) { - addExecutionError(t); - } - } - - private void reportDone() { - if (executionResult != null) { - addExecutionErrorsToTestExecutionResult(); - listener.executionFinished(testDescriptor, executionResult); - } - else if (executionErrors.isEmpty() && skipResult.isSkipped()) { - listener.executionSkipped(testDescriptor, skipResult.getReason().orElse("")); - } - else { - // Call executionStarted first to comply with the contract of EngineExecutionListener. - listener.executionStarted(testDescriptor); - listener.executionFinished(testDescriptor, createTestExecutionResultFromExecutionErrors()); - } - } - - private void addExecutionErrorsToTestExecutionResult() { - if (executionErrors.isEmpty()) { - return; - } - if (executionResult.getStatus() == FAILED && executionResult.getThrowable().isPresent()) { - Throwable throwable = executionResult.getThrowable().get(); - executionErrors.forEach(throwable::addSuppressed); - } - else { - executionResult = createTestExecutionResultFromExecutionErrors(); - } - } - - private TestExecutionResult createTestExecutionResultFromExecutionErrors() { - Throwable throwable = executionErrors.get(0); - executionErrors.stream().skip(1).forEach(throwable::addSuppressed); - return TestExecutionResult.failed(throwable); - } - - private void addExecutionError(Throwable throwable) { - rethrowIfBlacklisted(throwable); - executionErrors.add(throwable); - } - } - - @SuppressWarnings("unchecked") - private Node asNode(TestDescriptor testDescriptor) { - return (testDescriptor instanceof Node ? (Node) testDescriptor : noOpNode); + NodeTestTask prepareNodeTestTaskTree() { + TestDescriptor rootTestDescriptor = this.request.getRootTestDescriptor(); + EngineExecutionListener executionListener = this.request.getEngineExecutionListener(); + NodeTestTask rootTestTask = new NodeTestTask<>(rootTestDescriptor, executionListener, this.executorService); + new NodeTestTaskWalker().walk(rootTestTask); + return rootTestTask; } - - @SuppressWarnings("rawtypes") - private static final Node noOpNode = new Node() { - }; - } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorService.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorService.java new file mode 100644 index 000000000000..4733841beeb8 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorService.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.List; +import java.util.concurrent.Future; + +import org.apiguardian.api.API; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.support.hierarchical.Node.ExecutionMode; + +/** + * A closeable service that executes {@linkplain TestTask test tasks}. + * + * @see HierarchicalTestEngine#createExecutorService(ExecutionRequest) + * @see SameThreadHierarchicalTestExecutorService + * @see ForkJoinPoolHierarchicalTestExecutorService + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public interface HierarchicalTestExecutorService extends AutoCloseable { + + /** + * Submit the supplied {@linkplain TestTask test task} to be executed by + * this service. + * + *

Implementations may {@linkplain TestTask#execute() execute} the task + * asynchronously as long as its + * {@linkplain TestTask#getExecutionMode() execution mode} is + * {@linkplain ExecutionMode#CONCURRENT concurrent}. + * + *

Implementations must generally acquire and release the task's + * {@link TestTask#getResourceLock() resource lock} before and after its + * execution unless they execute all tests in the same thread which + * upholds the same guarantees. + * + * @param testTask the test task to be executed + * @return a future that the caller can use to wait for the task's execution + * to be finished + * @see #invokeAll(List) + */ + Future submit(TestTask testTask); + + /** + * Invoke all supplied {@linkplain TestTask test tasks} and block until + * their execution has finished. + * + *

Implementations may {@linkplain TestTask#execute() execute} one or + * multiple of the supplied tasks in parallel as long as their + * {@linkplain TestTask#getExecutionMode() execution mode} is + * {@linkplain ExecutionMode#CONCURRENT concurrent}. + * + *

Implementations must generally acquire and release each task's + * {@link TestTask#getResourceLock() resource lock} before and after its + * execution unless they execute all tests in the same thread which + * upholds the same guarantees. + * + * @param testTasks the test tasks to be executed + * @see #submit(TestTask) + */ + void invokeAll(List testTasks); + + /** + * Close this service and let it perform any required cleanup work. + * + *

For example, thread-based implementations should usually close their + * thread pools in this method. + */ + @Override + void close(); + + /** + * An executable task that represents a single test or container. + */ + interface TestTask { + + /** + * Get the {@linkplain ExecutionMode execution mode} of this task. + */ + ExecutionMode getExecutionMode(); + + /** + * Get the {@linkplain ResourceLock resource lock} of this task. + */ + ResourceLock getResourceLock(); + + /** + * Execute this task. + */ + void execute(); + + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/LockManager.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/LockManager.java new file mode 100644 index 000000000000..8b8bc2a71860 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/LockManager.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ; + +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * @since 1.3 + */ +class LockManager { + + private static final Comparator COMPARATOR = comparing(ExclusiveResource::getKey).thenComparing( + ExclusiveResource::getLockMode); + + private final Map locksByKey = new ConcurrentHashMap<>(); + + ResourceLock getLockForResources(Collection resources) { + List locks = getDistinctSortedLocks(resources); + return toResourceLock(locks); + } + + private List getDistinctSortedLocks(Collection resources) { + // @formatter:off + Map> resourcesByKey = resources.stream() + .distinct() + .sorted(COMPARATOR) + .collect(groupingBy(ExclusiveResource::getKey, LinkedHashMap::new, toList())); + return resourcesByKey.values().stream() + .map(resourcesWithSameKey -> resourcesWithSameKey.get(0)) + .map(resource -> { + ReadWriteLock lock = this.locksByKey.computeIfAbsent(resource.getKey(), + key -> new ReentrantReadWriteLock()); + return resource.getLockMode() == READ ? lock.readLock() : lock.writeLock(); + }) + .collect(toList()); + // @formatter:on + } + + private ResourceLock toResourceLock(List locks) { + int size = locks.size(); + if (size == 0) { + return NopLock.INSTANCE; + } + if (size == 1) { + return new SingleLock(locks.get(0)); + } + return new CompositeLock(locks); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java index e581c7b30200..1eee9b4e47f8 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/Node.java @@ -10,9 +10,12 @@ package org.junit.platform.engine.support.hierarchical; +import static java.util.Collections.emptySet; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import java.util.Optional; +import java.util.Set; import org.apiguardian.api.API; import org.junit.platform.commons.util.ToStringBuilder; @@ -26,7 +29,7 @@ * @since 1.0 * @see HierarchicalTestEngine */ -@API(status = MAINTAINED, since = "1.0") +@API(status = MAINTAINED, since = "1.0", consumers = "org.junit.platform.engine.support.hierarchical") public interface Node { /** @@ -112,6 +115,33 @@ default C execute(C context, DynamicTestExecutor dynamicTestExecutor) throws Exc default void after(C context) throws Exception { } + /** + * Get the set of {@linkplain ExclusiveResource resources} required to + * execute this node. + * + * @return the set of resources required by this node; never {@code null} + * but potentially empty + * @see ExclusiveResource + * @since 1.3 + */ + @API(status = EXPERIMENTAL, since = "1.3", consumers = "org.junit.platform.engine.support.hierarchical") + default Set getExclusiveResources() { + return emptySet(); + } + + /** + * Get the preferred of {@linkplain ExecutionMode execution mode} for + * parallel execution of this node. + * + * @return the preferred execution mode of this node; never {@code null} + * @see ExecutionMode + * @since 1.3 + */ + @API(status = EXPERIMENTAL, since = "1.3", consumers = "org.junit.platform.engine.support.hierarchical") + default ExecutionMode getExecutionMode() { + return ExecutionMode.CONCURRENT; + } + /** * The result of determining whether the execution of a given {@code context} * should be skipped. @@ -204,4 +234,24 @@ interface DynamicTestExecutor { } + /** + * Supported execution modes for parallel execution. + * + * @see Node#getExecutionMode() + * @since 1.3 + */ + @API(status = EXPERIMENTAL, since = "1.3", consumers = "org.junit.platform.engine.support.hierarchical") + enum ExecutionMode { + + /** + * Force execution in same thread as the parent node. + */ + SAME_THREAD, + + /** + * Allow concurrent execution with any other node. + */ + CONCURRENT + } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java new file mode 100644 index 000000000000..2b822b4df94d --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java @@ -0,0 +1,247 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.stream.Collectors.toCollection; +import static org.junit.platform.commons.util.BlacklistedExceptions.rethrowIfBlacklisted; +import static org.junit.platform.engine.TestExecutionResult.Status.FAILED; +import static org.junit.platform.engine.TestExecutionResult.failed; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Future; + +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService.TestTask; +import org.junit.platform.engine.support.hierarchical.Node.ExecutionMode; +import org.junit.platform.engine.support.hierarchical.Node.SkipResult; + +/** + * @since 1.3 + */ +class NodeTestTask implements TestTask { + + private static final SingleTestExecutor singleTestExecutor = new SingleTestExecutor(); + + private final List executionErrors = new ArrayList<>(); + private final TestDescriptor testDescriptor; + private final EngineExecutionListener listener; + private final HierarchicalTestExecutorService executorService; + private final Node node; + private final ExecutionMode executionMode; + private final Set exclusiveResources; + private final List> children; + + private ResourceLock resourceLock = NopLock.INSTANCE; + private Optional forcedExecutionMode = Optional.empty(); + + private C parentContext; + private C context; + + private SkipResult skipResult; + private TestExecutionResult executionResult; + + NodeTestTask(TestDescriptor testDescriptor, EngineExecutionListener listener, + HierarchicalTestExecutorService executorService) { + this.testDescriptor = testDescriptor; + this.listener = listener; + this.executorService = executorService; + node = asNode(testDescriptor); + executionMode = node.getExecutionMode(); + exclusiveResources = node.getExclusiveResources(); + // @formatter:off + children = testDescriptor.getChildren().stream() + .map(descriptor -> new NodeTestTask(descriptor, listener, executorService)) + .collect(toCollection(ArrayList::new)); + // @formatter:on + } + + public Set getExclusiveResources() { + return exclusiveResources; + } + + public List> getChildren() { + return children; + } + + @Override + public ResourceLock getResourceLock() { + return resourceLock; + } + + public void setResourceLock(ResourceLock resourceLock) { + this.resourceLock = resourceLock; + } + + @Override + public ExecutionMode getExecutionMode() { + return forcedExecutionMode.orElse(executionMode); + } + + public void setForcedExecutionMode(ExecutionMode forcedExecutionMode) { + this.forcedExecutionMode = Optional.of(forcedExecutionMode); + } + + public void setParentContext(C parentContext) { + this.parentContext = parentContext; + } + + @Override + public void execute() { + prepare(); + if (executionErrors.isEmpty()) { + checkWhetherSkipped(); + } + if (executionErrors.isEmpty() && !skipResult.isSkipped()) { + executeRecursively(); + } + if (context != null) { + cleanUp(); + } + reportCompletion(); + } + + private void prepare() { + executeSafely(() -> context = node.prepare(parentContext)); + } + + private void checkWhetherSkipped() { + executeSafely(() -> skipResult = node.shouldBeSkipped(context)); + } + + private void executeRecursively() { + listener.executionStarted(testDescriptor); + + executionResult = singleTestExecutor.executeSafely(() -> { + Throwable failure = null; + try { + context = node.before(context); + + List> futures = new ArrayList<>(); + context = node.execute(context, + dynamicTestDescriptor -> executeDynamicTest(dynamicTestDescriptor, futures)); + + children.forEach(child -> child.setParentContext(context)); + executorService.invokeAll(children); + + // using a for loop for the sake for ForkJoinPool's work stealing + for (Future future : futures) { + future.get(); + } + } + catch (Throwable t) { + failure = t; + } + finally { + executeAfter(failure); + } + }); + } + + private void executeDynamicTest(TestDescriptor dynamicTestDescriptor, List> futures) { + listener.dynamicTestRegistered(dynamicTestDescriptor); + NodeTestTask nodeTestTask = new NodeTestTask<>(dynamicTestDescriptor, listener, executorService); + Set exclusiveResources = nodeTestTask.getExclusiveResources(); + if (!exclusiveResources.isEmpty()) { + listener.executionStarted(dynamicTestDescriptor); + String message = "Dynamic test descriptors must not declare exclusive resources: " + exclusiveResources; + listener.executionFinished(dynamicTestDescriptor, failed(new JUnitException(message))); + } + else { + nodeTestTask.setParentContext(context); + futures.add(executorService.submit(nodeTestTask)); + } + } + + private void executeAfter(Throwable failure) throws Throwable { + try { + node.after(context); + } + catch (Throwable t) { + if (failure != null && failure != t) { + failure.addSuppressed(t); + } + else { + throw t; + } + } + if (failure != null) { + throw failure; + } + } + + private void cleanUp() { + executeSafely(() -> node.cleanUp(context)); + } + + private void reportCompletion() { + if (executionResult != null) { + addExecutionErrorsToTestExecutionResult(); + listener.executionFinished(testDescriptor, executionResult); + } + else if (executionErrors.isEmpty() && skipResult.isSkipped()) { + listener.executionSkipped(testDescriptor, skipResult.getReason().orElse("")); + } + else { + // Call executionStarted first to comply with the contract of EngineExecutionListener. + listener.executionStarted(testDescriptor); + listener.executionFinished(testDescriptor, createTestExecutionResultFromExecutionErrors()); + } + } + + private void addExecutionErrorsToTestExecutionResult() { + if (executionErrors.isEmpty()) { + return; + } + if (executionResult.getStatus() == FAILED && executionResult.getThrowable().isPresent()) { + Throwable throwable = executionResult.getThrowable().get(); + executionErrors.forEach(throwable::addSuppressed); + } + else { + executionResult = createTestExecutionResultFromExecutionErrors(); + } + } + + private TestExecutionResult createTestExecutionResultFromExecutionErrors() { + Throwable throwable = executionErrors.get(0); + executionErrors.stream().skip(1).forEach(throwable::addSuppressed); + return failed(throwable); + } + + private void executeSafely(Action action) { + try { + action.execute(); + } + catch (Throwable t) { + rethrowIfBlacklisted(t); + executionErrors.add(t); + } + } + + @FunctionalInterface + private interface Action { + void execute() throws Exception; + } + + @SuppressWarnings("unchecked") + private Node asNode(TestDescriptor testDescriptor) { + return (testDescriptor instanceof Node ? (Node) testDescriptor : noOpNode); + } + + @SuppressWarnings("rawtypes") + private static final Node noOpNode = new Node() { + }; +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskWalker.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskWalker.java new file mode 100644 index 000000000000..da5895a4e0dc --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskWalker.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +/** + * @since 1.3 + */ +class NodeTestTaskWalker { + + private final LockManager lockManager = new LockManager(); + + void walk(NodeTestTask nodeTestTask) { + if (nodeTestTask.getExclusiveResources().isEmpty()) { + nodeTestTask.getChildren().forEach(this::walk); + } + else { + Set allResources = new HashSet<>(nodeTestTask.getExclusiveResources()); + doForChildrenRecursively(nodeTestTask, child -> { + allResources.addAll(child.getExclusiveResources()); + child.setForcedExecutionMode(SAME_THREAD); + }); + nodeTestTask.setResourceLock(lockManager.getLockForResources(allResources)); + } + } + + private void doForChildrenRecursively(NodeTestTask parent, Consumer> consumer) { + parent.getChildren().forEach(child -> { + consumer.accept(child); + doForChildrenRecursively(child, consumer); + }); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NopLock.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NopLock.java new file mode 100644 index 000000000000..0000a2c85e18 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NopLock.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +/** + * @since 1.3 + */ +class NopLock implements ResourceLock { + + static final ResourceLock INSTANCE = new NopLock(); + + private NopLock() { + } + + @Override + public ResourceLock acquire() { + return this; + } + + @Override + public void release() { + // nothing to do + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfiguration.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfiguration.java new file mode 100644 index 000000000000..6816d89d6506 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfiguration.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.concurrent.ForkJoinPool; + +import org.apiguardian.api.API; + +/** + * Configuration to use for parallel test execution. + * + *

Instances of this class are intended to be used to configure + * implementations of {@link HierarchicalTestExecutorService}. Such + * implementations may use all of the properties in this class or + * only a subset. + * + * @see ForkJoinPoolHierarchicalTestExecutorService + * @see ParallelExecutionConfigurationStrategy + * @see DefaultParallelExecutionConfigurationStrategy + * + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public interface ParallelExecutionConfiguration { + + /** + * Get the parallelism to be used. + * + * @see ForkJoinPool#getParallelism() + */ + int getParallelism(); + + /** + * Get the minimum number of runnable threads to be used. + */ + int getMinimumRunnable(); + + /** + * Get the maximum thread pool size to be used. + */ + int getMaxPoolSize(); + + /** + * Get the thread pool size to be used. + */ + int getCorePoolSize(); + + /** + * Get the number of milliseconds for which inactive threads should be kept + * alive before terminating them and shrinking the thread pool. + */ + int getKeepAlive(); + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.java new file mode 100644 index 000000000000..df9ae7f90a60 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.platform.engine.ConfigurationParameters; + +/** + * A strategy to use for configuring parallel test execution. + * + * @see DefaultParallelExecutionConfigurationStrategy + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public interface ParallelExecutionConfigurationStrategy { + + /** + * Create a configuration for parallel test execution based on the supplied + * {@link ConfigurationParameters}. + */ + ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters); + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ResourceLock.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ResourceLock.java new file mode 100644 index 000000000000..75f07f7a5e9a --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ResourceLock.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * A lock for a one or more resources. + * + * @see HierarchicalTestExecutorService.TestTask#getResourceLock() + * + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public interface ResourceLock extends AutoCloseable { + + /** + * Acquire this resource lock, potentially blocking. + * + * @return this lock so it can easily be used in a try-with-resources + * statement. + * @throws InterruptedException when the calling thread is interrupted + * while waiting to acquire this lock. + */ + ResourceLock acquire() throws InterruptedException; + + /** + * Release this resource lock. + */ + void release(); + + @Override + default void close() { + release(); + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/SameThreadHierarchicalTestExecutorService.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/SameThreadHierarchicalTestExecutorService.java new file mode 100644 index 000000000000..040ebe6e675a --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/SameThreadHierarchicalTestExecutorService.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.List; +import java.util.concurrent.Future; + +import org.apiguardian.api.API; + +/** + * A simple {@linkplain HierarchicalTestExecutorService executor service} that + * executes all {@linkplain TestTask test tasks} in the caller's thread. + * + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public class SameThreadHierarchicalTestExecutorService implements HierarchicalTestExecutorService { + + @Override + public Future submit(TestTask testTask) { + testTask.execute(); + return completedFuture(null); + } + + @Override + public void invokeAll(List tasks) { + tasks.forEach(TestTask::execute); + } + + @Override + public void close() { + // nothing to do + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/SingleLock.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/SingleLock.java new file mode 100644 index 000000000000..b67f2479381c --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/SingleLock.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.locks.Lock; + +/** + * @since 1.3 + */ +class SingleLock implements ResourceLock { + + private final Lock lock; + + SingleLock(Lock lock) { + this.lock = lock; + } + + // for tests only + Lock getLock() { + return lock; + } + + @Override + public ResourceLock acquire() throws InterruptedException { + ForkJoinPool.managedBlock(new SingleLockManagedBlocker()); + return this; + } + + @Override + public void release() { + lock.unlock(); + } + + private class SingleLockManagedBlocker implements ForkJoinPool.ManagedBlocker { + private boolean acquired; + + @Override + public boolean block() throws InterruptedException { + lock.lockInterruptibly(); + acquired = true; + return true; + } + + @Override + public boolean isReleasable() { + return acquired || lock.tryLock(); + } + } +} diff --git a/junit-platform-engine/src/test/java/org/junit/platform/engine/test/event/ExecutionEvent.java b/junit-platform-engine/src/test/java/org/junit/platform/engine/test/event/ExecutionEvent.java index 1badcce3db17..0b1162305367 100644 --- a/junit-platform-engine/src/test/java/org/junit/platform/engine/test/event/ExecutionEvent.java +++ b/junit-platform-engine/src/test/java/org/junit/platform/engine/test/event/ExecutionEvent.java @@ -18,6 +18,7 @@ import static org.junit.platform.engine.test.event.ExecutionEvent.Type.SKIPPED; import static org.junit.platform.engine.test.event.ExecutionEvent.Type.STARTED; +import java.time.Instant; import java.util.Optional; import java.util.function.Predicate; @@ -70,16 +71,22 @@ public static Predicate byPayload(Class payloadClass, Pre return event -> event.getPayload(payloadClass).filter(predicate).isPresent(); } + private final Instant timestamp; private final ExecutionEvent.Type type; private final TestDescriptor testDescriptor; private final Object payload; private ExecutionEvent(ExecutionEvent.Type type, TestDescriptor testDescriptor, Object payload) { + this.timestamp = Instant.now(); this.type = type; this.testDescriptor = testDescriptor; this.payload = payload; } + public Instant getTimestamp() { + return timestamp; + } + public ExecutionEvent.Type getType() { return type; } @@ -97,6 +104,7 @@ public String toString() { // @formatter:off return new ToStringBuilder(this) .append("type", type) + .append("timestamp", timestamp) .append("testDescriptor", testDescriptor) .append("payload", payload) .toString(); 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 new file mode 100644 index 000000000000..a62cbeebbd72 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015-2018 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 + * + * http://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; +import org.junit.platform.engine.reporting.ReportEntry; + +/** + * Collection of constants related to {@link Launcher}. + * + * @see org.junit.platform.engine.ConfigurationParameters + * @since 1.3 + */ +@API(status = EXPERIMENTAL, since = "1.3") +public class LauncherConstants { + + /** + * Property name used to enable capturing output to {@link System#out}: + * {@value} + * + *

By default, output to {@link System#out} is not captured. + * + *

If enabled, the JUnit Platform captures the corresponding output and + * publishes it as a {@link ReportEntry} using the + * {@value #STDOUT_REPORT_ENTRY_KEY} key immediately before reporting the + * test identifier as finished. + * + * @see #STDOUT_REPORT_ENTRY_KEY + * @see ReportEntry + * @see TestExecutionListener#reportingEntryPublished(TestIdentifier, ReportEntry) + */ + public static final String CAPTURE_STDOUT_PROPERTY_NAME = "junit.platform.output.capture.stdout"; + + /** + * Property name used to enable capturing output to {@link System#err}: + * {@value} + * + *

By default, output to {@link System#err} is not captured. + * + *

If enabled, the JUnit Platform captures the corresponding output and + * publishes it as a {@link ReportEntry} using the + * {@value #STDERR_REPORT_ENTRY_KEY} key immediately before reporting the + * test identifier as finished. + * + * @see #STDERR_REPORT_ENTRY_KEY + * @see ReportEntry + * @see TestExecutionListener#reportingEntryPublished(TestIdentifier, ReportEntry) + */ + public static final String CAPTURE_STDERR_PROPERTY_NAME = "junit.platform.output.capture.stderr"; + + /** + * Property name used to configure the maximum number of bytes for buffering + * to use per thread and output type if output capturing is enabled: + * {@value} + * + *

Value must be an integer; defaults to {@value CAPTURE_MAX_BUFFER_DEFAULT}. + * + * @see #CAPTURE_MAX_BUFFER_DEFAULT + */ + public static final String CAPTURE_MAX_BUFFER_PROPERTY_NAME = "junit.platform.output.capture.maxBuffer"; + + /** + * Default maximum number of bytes for buffering to use per thread and + * output type if output capturing is enabled. + * + * @see #CAPTURE_MAX_BUFFER_PROPERTY_NAME + */ + public static final int CAPTURE_MAX_BUFFER_DEFAULT = 4 * 1024 * 1024; + + /** + * Key used to publish captured output to {@link System#out} as part of a + * {@link ReportEntry}: {@value} + */ + public static final String STDOUT_REPORT_ENTRY_KEY = "stdout"; + + /** + * Key used to publish captured output to {@link System#err} as part of a + * {@link ReportEntry}: {@value} + */ + public static final String STDERR_REPORT_ENTRY_KEY = "stderr"; + + private LauncherConstants() { + /* no-op */ + } + +} 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 76a5c77caee5..07208868d2c2 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 @@ -13,6 +13,7 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.logging.Logger; @@ -140,18 +141,33 @@ private Optional discoverEngineRoot(TestEngine testEngine, private void execute(Root root, ConfigurationParameters configurationParameters, TestExecutionListener... listeners) { - TestExecutionListenerRegistry listenerRegistry = buildListenerRegistryForExecution(listeners); - TestPlan testPlan = TestPlan.from(root.getEngineDescriptors()); + withInterceptedStreams(configurationParameters, listenerRegistry, testExecutionListener -> { + TestPlan testPlan = TestPlan.from(root.getEngineDescriptors()); + testExecutionListener.testPlanExecutionStarted(testPlan); + ExecutionListenerAdapter engineExecutionListener = new ExecutionListenerAdapter(testPlan, + testExecutionListener); + for (TestEngine testEngine : root.getTestEngines()) { + TestDescriptor testDescriptor = root.getTestDescriptorFor(testEngine); + execute(testEngine, + new ExecutionRequest(testDescriptor, engineExecutionListener, configurationParameters)); + } + testExecutionListener.testPlanExecutionFinished(testPlan); + }); + } + + private void withInterceptedStreams(ConfigurationParameters configurationParameters, + TestExecutionListenerRegistry listenerRegistry, Consumer action) { TestExecutionListener testExecutionListener = listenerRegistry.getCompositeTestExecutionListener(); - testExecutionListener.testPlanExecutionStarted(testPlan); - ExecutionListenerAdapter engineExecutionListener = new ExecutionListenerAdapter(testPlan, - testExecutionListener); - for (TestEngine testEngine : root.getTestEngines()) { - TestDescriptor testDescriptor = root.getTestDescriptorFor(testEngine); - execute(testEngine, new ExecutionRequest(testDescriptor, engineExecutionListener, configurationParameters)); + Optional streamInterceptingTestExecutionListener = StreamInterceptingTestExecutionListener.create( + configurationParameters, testExecutionListener::reportingEntryPublished); + streamInterceptingTestExecutionListener.ifPresent(listenerRegistry::registerListeners); + try { + action.accept(testExecutionListener); + } + finally { + streamInterceptingTestExecutionListener.ifPresent(StreamInterceptingTestExecutionListener::unregister); } - testExecutionListener.testPlanExecutionFinished(testPlan); } private TestExecutionListenerRegistry buildListenerRegistryForExecution(TestExecutionListener... listeners) { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java new file mode 100644 index 000000000000..67d29a226bef --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_MAX_BUFFER_DEFAULT; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_MAX_BUFFER_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.STDERR_REPORT_ENTRY_KEY; +import static org.junit.platform.launcher.LauncherConstants.STDOUT_REPORT_ENTRY_KEY; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; + +import org.junit.platform.commons.util.StringUtils; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.core.TestExecutionListenerRegistry.EagerTestExecutionListener; + +/** + * @since 1.3 + */ +class StreamInterceptingTestExecutionListener implements EagerTestExecutionListener { + + private final Optional stdoutInterceptor; + private final Optional stderrInterceptor; + private final BiConsumer reporter; + + static Optional create(ConfigurationParameters configurationParameters, + BiConsumer reporter) { + + boolean captureStdout = configurationParameters.getBoolean(CAPTURE_STDOUT_PROPERTY_NAME).orElse(false); + boolean captureStderr = configurationParameters.getBoolean(CAPTURE_STDERR_PROPERTY_NAME).orElse(false); + if (!captureStdout && !captureStderr) { + return Optional.empty(); + } + + int maxSize = configurationParameters.get(CAPTURE_MAX_BUFFER_PROPERTY_NAME, Integer::valueOf) // + .orElse(CAPTURE_MAX_BUFFER_DEFAULT); + + Optional stdoutInterceptor = captureStdout ? StreamInterceptor.registerStdout(maxSize) + : Optional.empty(); + Optional stderrInterceptor = captureStderr ? StreamInterceptor.registerStderr(maxSize) + : Optional.empty(); + + if ((!stdoutInterceptor.isPresent() && captureStdout) || (!stderrInterceptor.isPresent() && captureStderr)) { + stdoutInterceptor.ifPresent(StreamInterceptor::unregister); + stderrInterceptor.ifPresent(StreamInterceptor::unregister); + return Optional.empty(); + } + return Optional.of(new StreamInterceptingTestExecutionListener(stdoutInterceptor, stderrInterceptor, reporter)); + } + + private StreamInterceptingTestExecutionListener(Optional stdoutInterceptor, + Optional stderrInterceptor, BiConsumer reporter) { + this.stdoutInterceptor = stdoutInterceptor; + this.stderrInterceptor = stderrInterceptor; + this.reporter = reporter; + } + + void unregister() { + stdoutInterceptor.ifPresent(StreamInterceptor::unregister); + stderrInterceptor.ifPresent(StreamInterceptor::unregister); + } + + @Override + public void executionJustStarted(TestIdentifier testIdentifier) { + stdoutInterceptor.ifPresent(StreamInterceptor::capture); + stderrInterceptor.ifPresent(StreamInterceptor::capture); + } + + @Override + public void executionJustFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + Map map = new HashMap<>(); + String out = stdoutInterceptor.map(StreamInterceptor::consume).orElse(""); + if (StringUtils.isNotBlank(out)) { + map.put(STDOUT_REPORT_ENTRY_KEY, out); + } + String err = stderrInterceptor.map(StreamInterceptor::consume).orElse(""); + if (StringUtils.isNotBlank(err)) { + map.put(STDERR_REPORT_ENTRY_KEY, err); + } + if (!map.isEmpty()) { + reporter.accept(testIdentifier, ReportEntry.from(map)); + } + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java new file mode 100644 index 000000000000..f1ac64f94076 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * @since 1.3 + */ +class StreamInterceptor extends PrintStream { + + private final PrintStream originalStream; + private final Consumer unregisterAction; + private final int maxNumberOfBytesPerThread; + + private final ThreadLocal output = ThreadLocal.withInitial( + RewindableByteArrayOutputStream::new); + + static Optional registerStdout(int maxNumberOfBytesPerThread) { + return register(System.out, System::setOut, maxNumberOfBytesPerThread); + } + + static Optional registerStderr(int maxNumberOfBytesPerThread) { + return register(System.err, System::setErr, maxNumberOfBytesPerThread); + } + + static Optional register(PrintStream originalStream, Consumer streamSetter, + int maxNumberOfBytesPerThread) { + if (originalStream instanceof StreamInterceptor) { + return Optional.empty(); + } + StreamInterceptor interceptor = new StreamInterceptor(originalStream, streamSetter, maxNumberOfBytesPerThread); + streamSetter.accept(interceptor); + return Optional.of(interceptor); + } + + private StreamInterceptor(PrintStream originalStream, Consumer unregisterAction, + int maxNumberOfBytesPerThread) { + super(originalStream); + this.originalStream = originalStream; + this.unregisterAction = unregisterAction; + this.maxNumberOfBytesPerThread = maxNumberOfBytesPerThread; + } + + void capture() { + output.get().mark(); + } + + String consume() { + return output.get().rewind(); + } + + void unregister() { + unregisterAction.accept(originalStream); + } + + @Override + public void write(int b) { + RewindableByteArrayOutputStream out = output.get(); + if (out.isMarked() && out.size() < maxNumberOfBytesPerThread) { + out.write(b); + } + super.write(b); + } + + @Override + public void write(byte[] b) { + write(b, 0, b.length); + } + + @Override + public void write(byte[] buf, int off, int len) { + RewindableByteArrayOutputStream out = output.get(); + if (out.isMarked()) { + int actualLength = Math.max(0, Math.min(len, maxNumberOfBytesPerThread - out.size())); + if (actualLength > 0) { + out.write(buf, off, actualLength); + } + } + super.write(buf, off, len); + } + + class RewindableByteArrayOutputStream extends ByteArrayOutputStream { + + private final Deque markedPositions = new ArrayDeque<>(); + + boolean isMarked() { + return !markedPositions.isEmpty(); + } + + void mark() { + markedPositions.addFirst(count); + } + + String rewind() { + Integer position = markedPositions.pollFirst(); + if (position == null || position == count) { + return ""; + } + int length = count - position; + count -= length; + return new String(buf, position, length); + } + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/TestExecutionListenerRegistry.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/TestExecutionListenerRegistry.java index 4ba92998836a..5f22f528fad1 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/TestExecutionListenerRegistry.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/TestExecutionListenerRegistry.java @@ -11,6 +11,7 @@ package org.junit.platform.launcher.core; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -26,16 +27,17 @@ */ class TestExecutionListenerRegistry { - private final List testExecutionListeners; + private final List testExecutionListeners = new ArrayList<>(); + private final List eagerTestExecutionListeners = new ArrayList<>(); TestExecutionListenerRegistry() { this(null); } TestExecutionListenerRegistry(TestExecutionListenerRegistry source) { - this.testExecutionListeners = new ArrayList<>(); if (source != null) { this.testExecutionListeners.addAll(source.testExecutionListeners); + this.eagerTestExecutionListeners.addAll(source.eagerTestExecutionListeners); } } @@ -45,12 +47,22 @@ List getTestExecutionListeners() { void registerListeners(TestExecutionListener... listeners) { Collections.addAll(this.testExecutionListeners, listeners); + // @formatter:off + Arrays.stream(listeners) + .filter(EagerTestExecutionListener.class::isInstance) + .map(EagerTestExecutionListener.class::cast) + .forEach(this.eagerTestExecutionListeners::add); + // @formatter:on } private void notifyTestExecutionListeners(Consumer consumer) { this.testExecutionListeners.forEach(consumer); } + private void notifyEagerTestExecutionListeners(Consumer consumer) { + this.eagerTestExecutionListeners.forEach(consumer); + } + TestExecutionListener getCompositeTestExecutionListener() { return new CompositeTestExecutionListener(); } @@ -69,11 +81,14 @@ public void executionSkipped(TestIdentifier testIdentifier, String reason) { @Override public void executionStarted(TestIdentifier testIdentifier) { + notifyEagerTestExecutionListeners(listener -> listener.executionJustStarted(testIdentifier)); notifyTestExecutionListeners(listener -> listener.executionStarted(testIdentifier)); } @Override public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + notifyEagerTestExecutionListeners( + listener -> listener.executionJustFinished(testIdentifier, testExecutionResult)); notifyTestExecutionListeners(listener -> listener.executionFinished(testIdentifier, testExecutionResult)); } @@ -94,4 +109,12 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e } + interface EagerTestExecutionListener extends TestExecutionListener { + default void executionJustStarted(TestIdentifier testIdentifier) { + } + + default void executionJustFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + } + } + } diff --git a/junit-platform-surefire-provider/src/test/java/org/junit/platform/surefire/provider/JUnitPlatformProviderTests.java b/junit-platform-surefire-provider/src/test/java/org/junit/platform/surefire/provider/JUnitPlatformProviderTests.java index d3aa29a5534d..76dfe3a29b4e 100644 --- a/junit-platform-surefire-provider/src/test/java/org/junit/platform/surefire/provider/JUnitPlatformProviderTests.java +++ b/junit-platform-surefire-provider/src/test/java/org/junit/platform/surefire/provider/JUnitPlatformProviderTests.java @@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.mockito.AdditionalMatchers.gt; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; @@ -176,9 +175,9 @@ void outputIsCaptured() throws Exception { ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); // @formatter:off verify((ConsoleOutputReceiver) runListener) - .writeTestOutput(captor.capture(), eq(0), gt(6), eq(true)); + .writeTestOutput(captor.capture(), eq(0), eq(6), eq(true)); verify((ConsoleOutputReceiver) runListener) - .writeTestOutput(captor.capture(), eq(0), gt(6), eq(false)); + .writeTestOutput(captor.capture(), eq(0), eq(6), eq(false)); assertThat(captor.getAllValues()) .extracting(bytes -> new String(bytes, 0, 6)) .containsExactly("stdout", "stderr"); @@ -561,8 +560,8 @@ void test3() { static class VerboseTestClass { @Test void test() { - System.out.println("stdout"); - System.err.println("stderr"); + System.out.print("stdout"); + System.err.print("stderr"); } } diff --git a/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java b/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java index d6d78b09ca0b..3a5a46e2dc82 100644 --- a/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java +++ b/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java @@ -45,6 +45,8 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } interface Resource { + String ID = "org.junit.jupiter.extensions.Heavyweight.Resource"; + int usages(); } diff --git a/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightAlphaTests.java b/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightAlphaTests.java index 7bf1cc3b8fd9..434b6e6652e3 100644 --- a/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightAlphaTests.java +++ b/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightAlphaTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceLock; /** * Unit tests for {@link org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource} @@ -30,6 +31,7 @@ * @since 1.1 */ @ExtendWith(Heavyweight.class) +@ResourceLock(Heavyweight.Resource.ID) class HeavyweightAlphaTests { private static int mark; diff --git a/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightBetaTests.java b/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightBetaTests.java index 80c4e8ba954d..7169e03728c1 100644 --- a/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightBetaTests.java +++ b/platform-tests/src/test/java/org/junit/jupiter/extensions/HeavyweightBetaTests.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceLock; /** * Unit tests for {@link org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource} @@ -28,6 +29,7 @@ */ @ExtendWith(Heavyweight.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ResourceLock(Heavyweight.Resource.ID) class HeavyweightBetaTests { private int mark; diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/ClasspathScannerTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/ClasspathScannerTests.java index 7000e0f35c9d..014c97437762 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/ClasspathScannerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/ClasspathScannerTests.java @@ -130,8 +130,7 @@ private void assertDebugMessageLogged(LogRecordListener listener, String regex) assertThat(listener.stream(ClasspathScanner.class, Level.FINE) .map(LogRecord::getMessage) .filter(m -> m.matches(regex)) - .count() - ).isEqualTo(1); + ).hasSize(1); // @formatter:on } diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/XmlReportAssertions.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/XmlReportAssertions.java index a31a5b8cca71..8e625cc29b0c 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/XmlReportAssertions.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/XmlReportAssertions.java @@ -10,15 +10,17 @@ package org.junit.platform.console.tasks; +import static org.junit.jupiter.api.Assertions.fail; + import java.io.StringReader; import java.net.URL; import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; -import org.opentest4j.AssertionFailedError; import org.xml.sax.SAXException; /** @@ -26,24 +28,37 @@ */ class XmlReportAssertions { - private static Validator schemaValidator; - static void assertValidAccordingToJenkinsSchema(String content) throws Exception { try { - getSchemaValidator().validate(new StreamSource(new StringReader(content))); + // Schema is thread-safe, Validator is not + Validator validator = CachedSchema.JENKINS.newValidator(); + validator.validate(new StreamSource(new StringReader(content))); } catch (SAXException e) { - throw new AssertionFailedError("Invalid XML document: " + content, e); + fail("Invalid XML document: " + content, e); } } - private static Validator getSchemaValidator() throws SAXException { - if (schemaValidator == null) { - URL schemaFile = XmlReportsWritingListener.class.getResource("/jenkins-junit.xsd"); + private enum CachedSchema { + + JENKINS("/jenkins-junit.xsd"); + + private final Schema schema; + + CachedSchema(String resourcePath) { + URL schemaFile = XmlReportsWritingListener.class.getResource(resourcePath); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - schemaValidator = schemaFactory.newSchema(schemaFile).newValidator(); + try { + this.schema = schemaFactory.newSchema(schemaFile); + } + catch (SAXException e) { + throw new RuntimeException("Failed to create schema using " + schemaFile, e); + } + } + + Validator newValidator() { + return schema.newValidator(); } - return schemaValidator; } } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/config/PrefixedConfigurationParametersTest.java b/platform-tests/src/test/java/org/junit/platform/engine/support/config/PrefixedConfigurationParametersTest.java new file mode 100644 index 000000000000..0dab14bd2ab4 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/config/PrefixedConfigurationParametersTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.ConfigurationParameters; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PrefixedConfigurationParametersTests { + + @Mock + private ConfigurationParameters delegate; + + @Test + void delegatesGetCalls() { + when(delegate.get(any())).thenReturn(Optional.of("result")); + PrefixedConfigurationParameters parameters = new PrefixedConfigurationParameters(delegate, "foo.bar."); + + assertThat(parameters.get("qux")).contains("result"); + + verify(delegate).get("foo.bar.qux"); + } + + @Test + void delegatesGetBooleanCalls() { + when(delegate.getBoolean(any())).thenReturn(Optional.of(true)); + PrefixedConfigurationParameters parameters = new PrefixedConfigurationParameters(delegate, "foo.bar."); + + assertThat(parameters.getBoolean("qux")).contains(true); + + verify(delegate).getBoolean("foo.bar.qux"); + } + + @Test + void delegatesGetWithTransformerCalls() { + when(delegate.get(any(), any())).thenReturn(Optional.of("QUX")); + PrefixedConfigurationParameters parameters = new PrefixedConfigurationParameters(delegate, "foo.bar."); + + Function transformer = String::toUpperCase; + assertThat(parameters.get("qux", transformer)).contains("QUX"); + + verify(delegate).get("foo.bar.qux", transformer); + } + + @Test + void delegatesSizeCalls() { + when(delegate.size()).thenReturn(42); + PrefixedConfigurationParameters parameters = new PrefixedConfigurationParameters(delegate, "foo.bar."); + + assertThat(parameters.size()).isEqualTo(42); + + verify(delegate).size(); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/CompositeLockTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/CompositeLockTests.java new file mode 100644 index 000000000000..decc70ae443c --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/CompositeLockTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +/** + * @since 1.3 + */ +class CompositeLockTests { + + @Test + void acquiresAllLocksInOrder() throws Exception { + ReentrantLock lock1 = spy(new ReentrantLock()); + ReentrantLock lock2 = spy(new ReentrantLock()); + + new CompositeLock(asList(lock1, lock2)).acquire(); + + InOrder inOrder = inOrder(lock1, lock2); + inOrder.verify(lock1).lockInterruptibly(); + inOrder.verify(lock2).lockInterruptibly(); + assertTrue(lock1.isLocked()); + assertTrue(lock2.isLocked()); + } + + @Test + void releasesAllLocksInReverseOrder() throws Exception { + ReentrantLock lock1 = spy(new ReentrantLock()); + ReentrantLock lock2 = spy(new ReentrantLock()); + + new CompositeLock(asList(lock1, lock2)).acquire().close(); + + InOrder inOrder = inOrder(lock1, lock2); + inOrder.verify(lock2).unlock(); + inOrder.verify(lock1).unlock(); + assertFalse(lock1.isLocked()); + assertFalse(lock2.isLocked()); + } + + @Test + void releasesLocksInReverseOrderWhenInterruptedDuringAcquire() throws Exception { + CountDownLatch firstTwoLocksWereLocked = new CountDownLatch(2); + Lock firstLock = mockLock("firstLock", firstTwoLocksWereLocked); + Lock secondLock = mockLock("secondLock", firstTwoLocksWereLocked); + Lock unavailableLock = spy(new ReentrantLock()); + unavailableLock.lock(); + + Thread thread = new Thread(() -> { + try { + new CompositeLock(asList(firstLock, secondLock, unavailableLock)).acquire(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + thread.start(); + firstTwoLocksWereLocked.await(); + thread.interrupt(); + thread.join(); + + InOrder inOrder = inOrder(firstLock, secondLock); + inOrder.verify(secondLock).unlock(); + inOrder.verify(firstLock).unlock(); + verify(unavailableLock, never()).unlock(); + } + + private Lock mockLock(String name, CountDownLatch countDownWhenLocked) throws InterruptedException { + Lock lock = mock(Lock.class, name); + doAnswer(invocation -> { + countDownWhenLocked.countDown(); + return null; + }).when(lock).lockInterruptibly(); + return lock; + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategyTest.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategyTest.java new file mode 100644 index 000000000000..491fe0012cda --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategyTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.ConfigurationParameters; + +/** + * @since 1.3 + */ +class DefaultParallelExecutionConfigurationStrategyTest { + + private ConfigurationParameters configParams = mock(ConfigurationParameters.class); + + @Test + void fixedStrategyCreatesValidConfiguration() { + when(configParams.get("fixed.parallelism")).thenReturn(Optional.of("42")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.FIXED; + ParallelExecutionConfiguration configuration = strategy.createConfiguration(configParams); + + assertThat(configuration.getParallelism()).isEqualTo(42); + assertThat(configuration.getCorePoolSize()).isEqualTo(42); + assertThat(configuration.getMinimumRunnable()).isEqualTo(42); + assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + 42); + assertThat(configuration.getKeepAlive()).isEqualTo(30); + } + + @Test + void dynamicStrategyCreatesValidConfiguration() { + when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC; + ParallelExecutionConfiguration configuration = strategy.createConfiguration(configParams); + + int availableProcessors = Runtime.getRuntime().availableProcessors(); + assertThat(configuration.getParallelism()).isEqualTo(availableProcessors * 2); + assertThat(configuration.getCorePoolSize()).isEqualTo(availableProcessors * 2); + assertThat(configuration.getMinimumRunnable()).isEqualTo(availableProcessors * 2); + assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + (availableProcessors * 2)); + assertThat(configuration.getKeepAlive()).isEqualTo(30); + } + + @Test + void customStrategyCreatesValidConfiguration() { + when(configParams.get("custom.class")).thenReturn( + Optional.of(CustomParallelExecutionConfigurationStrategy.class.getName())); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.CUSTOM; + ParallelExecutionConfiguration configuration = strategy.createConfiguration(configParams); + + assertThat(configuration.getParallelism()).isEqualTo(1); + assertThat(configuration.getCorePoolSize()).isEqualTo(4); + assertThat(configuration.getMinimumRunnable()).isEqualTo(2); + assertThat(configuration.getMaxPoolSize()).isEqualTo(3); + assertThat(configuration.getKeepAlive()).isEqualTo(5); + } + + @ParameterizedTest + @EnumSource(DefaultParallelExecutionConfigurationStrategy.class) + void createsStrategyFromConfigParam(DefaultParallelExecutionConfigurationStrategy strategy) { + when(configParams.get("strategy")).thenReturn(Optional.of(strategy.name().toLowerCase())); + + assertThat(DefaultParallelExecutionConfigurationStrategy.getStrategy(configParams)).isSameAs(strategy); + } + + @Test + void fixedStrategyThrowsExceptionWhenPropertyIsNotPresent() { + when(configParams.get("fixed.parallelism")).thenReturn(Optional.empty()); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.FIXED; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + @Test + void fixedStrategyThrowsExceptionWhenPropertyIsNotAnInteger() { + when(configParams.get("fixed.parallelism")).thenReturn(Optional.of("foo")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.FIXED; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + @Test + void dynamicStrategyUsesDefaultWhenPropertyIsNotPresent() { + when(configParams.get("dynamic.factor")).thenReturn(Optional.empty()); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC; + ParallelExecutionConfiguration configuration = strategy.createConfiguration(configParams); + + int availableProcessors = Runtime.getRuntime().availableProcessors(); + assertThat(configuration.getParallelism()).isEqualTo(availableProcessors); + assertThat(configuration.getCorePoolSize()).isEqualTo(availableProcessors); + assertThat(configuration.getMinimumRunnable()).isEqualTo(availableProcessors); + assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + availableProcessors); + assertThat(configuration.getKeepAlive()).isEqualTo(30); + } + + @Test + void dynamicStrategyThrowsExceptionWhenPropertyIsNotAnInteger() { + when(configParams.get("dynamic.factor")).thenReturn(Optional.of("foo")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + @Test + void dynamicStrategyThrowsExceptionWhenFactorIsZero() { + when(configParams.get("dynamic.factor")).thenReturn(Optional.of("0")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + @Test + void dynamicStrategyThrowsExceptionWhenFactorIsNegative() { + when(configParams.get("dynamic.factor")).thenReturn(Optional.of("-1")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + @Test + void dynamicStrategyUsesAtLeastParallelismOfOneWhenPropertyIsTooSmall() { + when(configParams.get("dynamic.factor")).thenReturn(Optional.of("0.00000000001")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC; + ParallelExecutionConfiguration configuration = strategy.createConfiguration(configParams); + + assertThat(configuration.getParallelism()).isEqualTo(1); + assertThat(configuration.getCorePoolSize()).isEqualTo(1); + assertThat(configuration.getMinimumRunnable()).isEqualTo(1); + assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + 1); + assertThat(configuration.getKeepAlive()).isEqualTo(30); + } + + @Test + void customStrategyThrowsExceptionWhenPropertyIsNotPresent() { + when(configParams.get("custom.class")).thenReturn(Optional.empty()); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.CUSTOM; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + @Test + void customStrategyThrowsExceptionWhenClassDoesNotExist() { + when(configParams.get("custom.class")).thenReturn(Optional.of("com.acme.ClassDoesNotExist")); + + ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.CUSTOM; + assertThrows(JUnitException.class, () -> strategy.createConfiguration(configParams)); + } + + static class CustomParallelExecutionConfigurationStrategy implements ParallelExecutionConfigurationStrategy { + @Override + public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) { + return new DefaultParallelExecutionConfiguration(1, 2, 3, 4, 5); + } + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java index 4ffbfa709957..75d5c6125705 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java @@ -10,6 +10,7 @@ package org.junit.platform.engine.support.hierarchical; +import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -23,7 +24,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import org.junit.jupiter.api.BeforeEach; @@ -35,6 +35,7 @@ import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode; import org.junit.platform.engine.support.hierarchical.Node.DynamicTestExecutor; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; @@ -148,7 +149,7 @@ void skippingAContainer() throws Exception { inOrder.verify(listener).executionFinished(eq(root), any(TestExecutionResult.class)); verify(listener, never()).executionStarted(child); - verifyNoMoreInteractions(child); + verify(child, never()).execute(any(), any()); verify(listener, never()).executionFinished(eq(child), any(TestExecutionResult.class)); } @@ -170,7 +171,7 @@ void skippingALeaf() throws Exception { inOrder.verify(listener).executionFinished(eq(root), any(TestExecutionResult.class)); verify(listener, never()).executionStarted(child); - verifyNoMoreInteractions(child); + verify(child, never()).execute(any(), any()); verify(listener, never()).executionFinished(eq(child), any(TestExecutionResult.class)); } @@ -195,7 +196,7 @@ void exceptionInShouldBeSkipped() throws Exception { inOrder.verify(listener).executionFinished(eq(child), childExecutionResult.capture()); inOrder.verify(listener).executionFinished(eq(root), any(TestExecutionResult.class)); - verifyNoMoreInteractions(child); + verify(child, never()).execute(any(), any()); assertThat(childExecutionResult.getValue().getStatus()).isEqualTo(FAILED); assertThat(childExecutionResult.getValue().getThrowable()).containsSame(anException); @@ -224,7 +225,7 @@ void exceptionInContainerBeforeAll() throws Exception { assertThat(rootExecutionResult.getValue().getStatus()).isEqualTo(FAILED); assertThat(rootExecutionResult.getValue().getThrowable()).containsSame(anException); - verifyNoMoreInteractions(child); + verify(child, never()).execute(any(), any()); } @Test @@ -371,7 +372,7 @@ void abortInContainerBeforeAll() throws Exception { assertThat(rootExecutionResult.getValue().getStatus()).isEqualTo(ABORTED); assertThat(rootExecutionResult.getValue().getThrowable()).containsSame(anAbortedException); - verifyNoMoreInteractions(child); + verify(child, never()).execute(any(), any()); } @Test @@ -545,6 +546,35 @@ void exceptionInAfterDoesNotHideEarlierException() throws Exception { exceptionInExecute).hasSuppressedException(exceptionInAfter); } + @Test + void dynamicTestDescriptorsMustNotDeclareExclusiveResources() throws Exception { + + UniqueId leafUniqueId = UniqueId.root("leaf", "child leaf"); + MyLeaf child = spy(new MyLeaf(leafUniqueId)); + MyLeaf dynamicTestDescriptor = spy(new MyLeaf(leafUniqueId.append("dynamic", "child"))); + when(dynamicTestDescriptor.getExclusiveResources()).thenReturn( + singleton(new ExclusiveResource("foo", LockMode.READ))); + + when(child.execute(any(), any())).thenAnswer(invocation -> { + DynamicTestExecutor dynamicTestExecutor = invocation.getArgument(1); + dynamicTestExecutor.execute(dynamicTestDescriptor); + return invocation.getArgument(0); + }); + root.addChild(child); + + executor.execute(); + + ArgumentCaptor aTestExecutionResult = ArgumentCaptor.forClass(TestExecutionResult.class); + verify(listener).executionStarted(dynamicTestDescriptor); + verify(listener).executionFinished(eq(dynamicTestDescriptor), aTestExecutionResult.capture()); + + TestExecutionResult executionResult = aTestExecutionResult.getValue(); + assertThat(executionResult.getStatus()).isEqualTo(FAILED); + assertThat(executionResult.getThrowable()).isPresent(); + assertThat(executionResult.getThrowable().get()).hasMessageContaining( + "Dynamic test descriptors must not declare exclusive resources"); + } + // ------------------------------------------------------------------- private static class MyEngineExecutionContext implements EngineExecutionContext { @@ -596,7 +626,7 @@ public Type getType() { private static class MyExecutor extends HierarchicalTestExecutor { MyExecutor(ExecutionRequest request, MyEngineExecutionContext rootContext) { - super(request, rootContext); + super(request, rootContext, new SameThreadHierarchicalTestExecutorService()); } } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/LockManagerTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/LockManagerTests.java new file mode 100644 index 000000000000..1e483003566e --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/LockManagerTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ; +import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ_WRITE; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import org.junit.jupiter.api.Test; + +/** + * @since 1.3 + */ +class LockManagerTests { + + private LockManager lockManager = new LockManager(); + + @Test + void returnsNopLockWithoutExclusiveResources() { + Collection resources = emptySet(); + + List locks = getLocks(resources, NopLock.class); + + assertThat(locks).isEmpty(); + } + + @Test + void returnsSingleLockForSingleExclusiveResource() { + Collection resources = singleton(new ExclusiveResource("foo", READ)); + + List locks = getLocks(resources, SingleLock.class); + + assertThat(locks).hasSize(1); + assertThat(locks.get(0)).isInstanceOf(ReadLock.class); + } + + @Test + void returnsCompositeLockForMultipleDifferentExclusiveResources() { + Collection resources = asList( // + new ExclusiveResource("a", READ), // + new ExclusiveResource("b", READ_WRITE)); + + List locks = getLocks(resources, CompositeLock.class); + + assertThat(locks).hasSize(2); + assertThat(locks.get(0)).isInstanceOf(ReadLock.class); + assertThat(locks.get(1)).isInstanceOf(WriteLock.class); + } + + @Test + void reusesSameLockForExclusiveResourceWithSameKey() { + Collection resources = singleton(new ExclusiveResource("foo", READ)); + + List locks1 = getLocks(resources, SingleLock.class); + List locks2 = getLocks(resources, SingleLock.class); + + assertThat(locks1).hasSize(1); + assertThat(locks2).hasSize(1); + assertThat(locks1.get(0)).isSameAs(locks2.get(0)); + } + + @Test + void returnsWriteLockForExclusiveResourceWithBothLockModes() { + Collection resources = asList( // + new ExclusiveResource("bar", READ), // + new ExclusiveResource("foo", READ), // + new ExclusiveResource("foo", READ_WRITE), // + new ExclusiveResource("bar", READ_WRITE)); + + List locks = getLocks(resources, CompositeLock.class); + + assertThat(locks).hasSize(2); + assertThat(locks.get(0)).isInstanceOf(WriteLock.class); + assertThat(locks.get(1)).isInstanceOf(WriteLock.class); + } + + private List getLocks(Collection resources, Class type) { + ResourceLock lock = lockManager.getLockForResources(resources); + assertThat(lock).isInstanceOf(type); + return getLocks(lock); + } + + private List getLocks(ResourceLock resourceLock) { + if (resourceLock instanceof NopLock) { + return emptyList(); + } + if (resourceLock instanceof SingleLock) { + return singletonList(((SingleLock) resourceLock).getLock()); + } + return ((CompositeLock) resourceLock).getLocks(); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskWalkerIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskWalkerIntegrationTests.java new file mode 100644 index 000000000000..d189928f4e0c --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskWalkerIntegrationTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.Node.ExecutionMode; +import org.junit.platform.launcher.LauncherDiscoveryRequest; + +/** + * @since 1.3 + */ +class NodeTestTaskWalkerIntegrationTests { + + @Test + void pullUpExclusiveChildResourcesToTestClass() { + NodeTestTask engineNodeTestTask = prepareNodeTestTaskTree(TestCaseWithResourceLock.class); + + assertThat(engineNodeTestTask.getChildren()).hasSize(1); + NodeTestTask testClassExecutor = engineNodeTestTask.getChildren().get(0); + assertThat(testClassExecutor.getResourceLock()).isInstanceOf(CompositeLock.class); + assertThat(testClassExecutor.getExecutionMode()).isEqualTo(ExecutionMode.CONCURRENT); + + assertThat(testClassExecutor.getChildren()).hasSize(1); + NodeTestTask testMethodExecutor = testClassExecutor.getChildren().get(0); + assertThat(testMethodExecutor.getResourceLock()).isInstanceOf(NopLock.class); + assertThat(testMethodExecutor.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD); + } + + @Test + void leavesResourceLockOnTestMethodWhenClassDoesNotUseResource() { + NodeTestTask engineNodeTestTask = prepareNodeTestTaskTree(TestCaseWithoutResourceLock.class); + + assertThat(engineNodeTestTask.getChildren()).hasSize(1); + NodeTestTask testClassExecutor = engineNodeTestTask.getChildren().get(0); + assertThat(testClassExecutor.getResourceLock()).isInstanceOf(NopLock.class); + assertThat(testClassExecutor.getExecutionMode()).isEqualTo(ExecutionMode.CONCURRENT); + + assertThat(testClassExecutor.getChildren()).hasSize(2); + NodeTestTask testMethodExecutor = testClassExecutor.getChildren().get(0); + assertThat(testMethodExecutor.getResourceLock()).isInstanceOf(SingleLock.class); + assertThat(testMethodExecutor.getExecutionMode()).isEqualTo(ExecutionMode.CONCURRENT); + + NodeTestTask nestedTestClassExecutor = testClassExecutor.getChildren().get(1); + assertThat(nestedTestClassExecutor.getResourceLock()).isInstanceOf(CompositeLock.class); + assertThat(nestedTestClassExecutor.getExecutionMode()).isEqualTo(ExecutionMode.CONCURRENT); + + assertThat(nestedTestClassExecutor.getChildren()).hasSize(1); + NodeTestTask nestedTestMethodExecutor = nestedTestClassExecutor.getChildren().get(0); + assertThat(nestedTestMethodExecutor.getResourceLock()).isInstanceOf(NopLock.class); + assertThat(nestedTestMethodExecutor.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD); + } + + private NodeTestTask prepareNodeTestTaskTree(Class testClass) { + LauncherDiscoveryRequest discoveryRequest = request().selectors(selectClass(testClass)).build(); + TestDescriptor testDescriptor = new JupiterTestEngine().discover(discoveryRequest, + UniqueId.forEngine("junit-jupiter")); + ExecutionRequest executionRequest = new ExecutionRequest(testDescriptor, null, null); + HierarchicalTestExecutor executor = new HierarchicalTestExecutor<>(executionRequest, null, null); + return executor.prepareNodeTestTaskTree(); + } + + @ResourceLock("a") + static class TestCaseWithResourceLock { + @Test + @ResourceLock("b") + void test() { + } + } + + static class TestCaseWithoutResourceLock { + @Test + @ResourceLock("a") + void test() { + } + + @Nested + @ResourceLock("c") + class NestedTestCaseWithResourceLock { + @Test + @ResourceLock("b") + void test() { + } + } + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java new file mode 100644 index 000000000000..f18fc63a19a2 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java @@ -0,0 +1,441 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; +import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME; +import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME; +import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; +import static org.junit.platform.engine.test.event.ExecutionEvent.Type.REPORTING_ENTRY_PUBLISHED; +import static org.junit.platform.engine.test.event.ExecutionEventConditions.event; +import static org.junit.platform.engine.test.event.ExecutionEventConditions.finishedSuccessfully; +import static org.junit.platform.engine.test.event.ExecutionEventConditions.finishedWithFailure; +import static org.junit.platform.engine.test.event.ExecutionEventConditions.started; +import static org.junit.platform.engine.test.event.ExecutionEventConditions.test; +import static org.junit.platform.engine.test.event.ExecutionEventConditions.type; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.test.event.ExecutionEvent; +import org.junit.platform.engine.test.event.ExecutionEventRecorder; +import org.junit.platform.launcher.LauncherDiscoveryRequest; + +/** + * @since 1.3 + */ +class ParallelExecutionIntegrationTests { + + @Test + void successfulParallelTest(TestReporter reporter) { + List executionEvents = execute(3, SuccessfulParallelTestCase.class); + + List startedTimestamps = getTimestampsFor(executionEvents, event(test(), started())); + List finishedTimestamps = getTimestampsFor(executionEvents, event(test(), finishedSuccessfully())); + reporter.publishEntry("startedTimestamps", startedTimestamps.toString()); + reporter.publishEntry("finishedTimestamps", finishedTimestamps.toString()); + + assertThat(startedTimestamps).hasSize(3); + assertThat(finishedTimestamps).hasSize(3); + assertThat(startedTimestamps).allMatch(startTimestamp -> finishedTimestamps.stream().allMatch( + finishedTimestamp -> !finishedTimestamp.isBefore(startTimestamp))); + assertThat(ThreadReporter.getThreadNames(executionEvents)).hasSize(3); + } + + @Test + void failingTestWithoutLock() { + List executionEvents = execute(3, FailingTestWithoutLock.class); + assertThat(executionEvents.stream().filter(event(test(), finishedWithFailure())::matches)).hasSize(2); + } + + @Test + void successfulTestWithMethodLock() { + List executionEvents = execute(3, SuccessfulTestWithMethodLock.class); + + assertThat(executionEvents.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3); + assertThat(ThreadReporter.getThreadNames(executionEvents)).hasSize(3); + } + + @Test + void successfulTestWithClassLock() { + List executionEvents = execute(3, SuccessfulTestWithClassLock.class); + + assertThat(executionEvents.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3); + assertThat(ThreadReporter.getThreadNames(executionEvents)).hasSize(1); + } + + @Test + void testCaseWithFactory() { + List executionEvents = execute(3, TestCaseWithTestFactory.class); + + assertThat(executionEvents.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3); + assertThat(ThreadReporter.getThreadNames(executionEvents)).hasSize(1); + } + + @RepeatedTest(10) + void mixingClassAndMethodLevelLocks() { + List executionEvents = execute(4, TestCaseWithSortedLocks.class, + TestCaseWithUnsortedLocks.class); + + assertThat(executionEvents.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(6); + assertThat(ThreadReporter.getThreadNames(executionEvents).count()).isLessThanOrEqualTo(2); + } + + @RepeatedTest(10) + void locksOnNestedTests() { + List executionEvents = execute(3, TestCaseWithNestedLocks.class); + + assertThat(executionEvents.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(6); + assertThat(ThreadReporter.getThreadNames(executionEvents)).hasSize(1); + } + + private List getTimestampsFor(List executionEvents, Condition condition) { + // @formatter:off + return executionEvents.stream() + .filter(condition::matches) + .map(ExecutionEvent::getTimestamp) + .collect(toList()); + // @formatter:on + } + + @ExtendWith(ThreadReporter.class) + static class SuccessfulParallelTestCase { + + static AtomicInteger sharedResource; + static CountDownLatch countDownLatch; + + @BeforeAll + static void initialize() { + sharedResource = new AtomicInteger(); + countDownLatch = new CountDownLatch(3); + } + + @Test + void firstTest(TestReporter reporter) throws Exception { + incrementAndBlock(sharedResource, countDownLatch); + } + + @Test + void secondTest(TestReporter reporter) throws Exception { + incrementAndBlock(sharedResource, countDownLatch); + } + + @Test + void thirdTest(TestReporter reporter) throws Exception { + incrementAndBlock(sharedResource, countDownLatch); + } + } + + @ExtendWith(ThreadReporter.class) + static class FailingTestWithoutLock { + + static AtomicInteger sharedResource; + static CountDownLatch countDownLatch; + + @BeforeAll + static void initialize() { + sharedResource = new AtomicInteger(); + countDownLatch = new CountDownLatch(3); + } + + @Test + void firstTest(TestReporter reporter) throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + + @Test + void secondTest(TestReporter reporter) throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + + @Test + void thirdTest(TestReporter reporter) throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + } + + @ExtendWith(ThreadReporter.class) + static class SuccessfulTestWithMethodLock { + + static AtomicInteger sharedResource; + static CountDownLatch countDownLatch; + + @BeforeAll + static void initialize() { + sharedResource = new AtomicInteger(); + countDownLatch = new CountDownLatch(3); + } + + @Test + @ResourceLock("sharedResource") + void firstTest(TestReporter reporter) throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + + @Test + @ResourceLock("sharedResource") + void secondTest(TestReporter reporter) throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + + @Test + @ResourceLock("sharedResource") + void thirdTest(TestReporter reporter) throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + } + + @ExtendWith(ThreadReporter.class) + @ResourceLock("sharedResource") + static class SuccessfulTestWithClassLock { + + static AtomicInteger sharedResource; + static CountDownLatch countDownLatch; + + @BeforeAll + static void initialize() { + sharedResource = new AtomicInteger(); + countDownLatch = new CountDownLatch(3); + } + + @Test + void firstTest() throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + + @Test + void secondTest() throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + + @Test + void thirdTest() throws Exception { + incrementBlockAndCheck(sharedResource, countDownLatch); + } + } + + static class TestCaseWithTestFactory { + @TestFactory + @Execution(SAME_THREAD) + Stream testFactory(TestReporter testReporter) { + AtomicInteger sharedResource = new AtomicInteger(0); + CountDownLatch countDownLatch = new CountDownLatch(3); + return IntStream.range(0, 3).mapToObj(i -> dynamicTest("test " + i, () -> { + incrementBlockAndCheck(sharedResource, countDownLatch); + testReporter.publishEntry(ThreadReporter.KEY, Thread.currentThread().getName()); + })); + } + } + + private static final ReentrantLock A = new ReentrantLock(); + private static final ReentrantLock B = new ReentrantLock(); + + @ExtendWith(ThreadReporter.class) + @ResourceLock("A") + static class TestCaseWithSortedLocks { + @ResourceLock("B") + @Test + void firstTest() { + assertTrue(A.tryLock()); + assertTrue(B.tryLock()); + } + + @Execution(CONCURRENT) + @ResourceLock("B") + @Test + void secondTest() { + assertTrue(A.tryLock()); + assertTrue(B.tryLock()); + } + + @ResourceLock("B") + @Test + void thirdTest() { + assertTrue(A.tryLock()); + assertTrue(B.tryLock()); + } + + @AfterEach + void unlock() { + B.unlock(); + A.unlock(); + } + } + + @ExtendWith(ThreadReporter.class) + @ResourceLock("B") + static class TestCaseWithUnsortedLocks { + @ResourceLock("A") + @Test + void firstTest() { + assertTrue(B.tryLock()); + assertTrue(A.tryLock()); + } + + @Execution(CONCURRENT) + @ResourceLock("A") + @Test + void secondTest() { + assertTrue(B.tryLock()); + assertTrue(A.tryLock()); + } + + @ResourceLock("A") + @Test + void thirdTest() { + assertTrue(B.tryLock()); + assertTrue(A.tryLock()); + } + + @AfterEach + void unlock() { + A.unlock(); + B.unlock(); + } + } + + @ExtendWith(ThreadReporter.class) + @ResourceLock("A") + static class TestCaseWithNestedLocks { + + @ResourceLock("B") + @Test + void firstTest() { + assertTrue(A.tryLock()); + assertTrue(B.tryLock()); + } + + @Execution(CONCURRENT) + @ResourceLock("B") + @Test + void secondTest() { + assertTrue(A.tryLock()); + assertTrue(B.tryLock()); + } + + @Test + void thirdTest() { + assertTrue(A.tryLock()); + assertTrue(B.tryLock()); + } + + @AfterEach + void unlock() { + A.unlock(); + B.unlock(); + } + + @Nested + @ResourceLock("B") + class B { + + @ResourceLock("A") + @Test + void firstTest() { + assertTrue(B.tryLock()); + assertTrue(A.tryLock()); + } + + @ResourceLock("A") + @Test + void secondTest() { + assertTrue(B.tryLock()); + assertTrue(A.tryLock()); + } + + @Test + void thirdTest() { + assertTrue(B.tryLock()); + assertTrue(A.tryLock()); + } + } + } + + private static void incrementBlockAndCheck(AtomicInteger sharedResource, CountDownLatch countDownLatch) + throws InterruptedException { + int value = incrementAndBlock(sharedResource, countDownLatch); + assertEquals(value, sharedResource.get()); + } + + private static int incrementAndBlock(AtomicInteger sharedResource, CountDownLatch countDownLatch) + throws InterruptedException { + int value = sharedResource.incrementAndGet(); + countDownLatch.countDown(); + countDownLatch.await(1, SECONDS); + return value; + } + + private List execute(int parallelism, Class... testClasses) { + // @formatter:off + LauncherDiscoveryRequest discoveryRequest = request() + .selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).collect(toList())) + .configurationParameter(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, String.valueOf(true)) + .configurationParameter(PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME, "fixed") + .configurationParameter(PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME, String.valueOf(parallelism)) + .build(); + // @formatter:on + return ExecutionEventRecorder.execute(new JupiterTestEngine(), discoveryRequest); + } + + static class ThreadReporter implements AfterTestExecutionCallback { + + public static final String KEY = "thread"; + + private static Stream getThreadNames(List executionEvents) { + // @formatter:off + return executionEvents.stream() + .filter(type(REPORTING_ENTRY_PUBLISHED)::matches) + .map(event -> event.getPayload(ReportEntry.class).orElse(null)) + .map(ReportEntry::getKeyValuePairs) + .filter(keyValuePairs -> keyValuePairs.containsKey(KEY)) + .map(keyValuePairs -> keyValuePairs.get("thread")) + .distinct(); + // @formatter:on + } + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + context.publishReportEntry("thread", Thread.currentThread().getName()); + } + } + +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/SingleLockTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/SingleLockTests.java new file mode 100644 index 000000000000..3bed5924d9a6 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/SingleLockTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.hierarchical; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.jupiter.api.Test; + +/** + * @since 1.3 + */ +class SingleLockTests { + + @Test + void acquire() throws Exception { + ReentrantLock lock = new ReentrantLock(); + + new SingleLock(lock).acquire(); + + assertTrue(lock.isLocked()); + } + + @Test + void release() throws Exception { + ReentrantLock lock = new ReentrantLock(); + + new SingleLock(lock).acquire().close(); + + assertFalse(lock.isLocked()); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherConfigurationParametersTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherConfigurationParametersTests.java index e178ce0a0cdb..7b7af179cc06 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherConfigurationParametersTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherConfigurationParametersTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.PreconditionViolationException; import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.discovery.DiscoverySelectors; @@ -129,6 +130,22 @@ void getValueInExtensionContext() { assertEquals(0, summary.getSummary().getTestsFailedCount()); } + @Test + void getWithSuccessfulTransformer() { + ConfigurationParameters configParams = fromMap(singletonMap(KEY, "42")); + assertThat(configParams.get(KEY, Integer::valueOf)).contains(42); + } + + @Test + void getWithErroneousTransformer() { + ConfigurationParameters configParams = fromMap(singletonMap(KEY, "42")); + JUnitException exception = assertThrows(JUnitException.class, () -> configParams.get(KEY, input -> { + throw new RuntimeException("foo"); + })); + assertThat(exception).hasMessageContaining( + "Failed to transform configuration parameter with key '" + KEY + "' and initial value '42'"); + } + private static LauncherConfigurationParameters fromMap(Map map) { return new LauncherConfigurationParameters(map); } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java new file mode 100644 index 000000000000..c3841fb3c584 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.junit.platform.engine.TestExecutionResult.successful; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.STDERR_REPORT_ENTRY_KEY; +import static org.junit.platform.launcher.LauncherConstants.STDOUT_REPORT_ENTRY_KEY; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.hierarchical.DemoHierarchicalTestEngine; +import org.junit.platform.launcher.LauncherConstants; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; + +/** + * @since 1.3 + */ +class StreamInterceptingTestExecutionListenerIntegrationTests { + + @ParameterizedTest(name = "{0}") + @MethodSource("systemStreams") + @ExtendWith(HiddenSystemOutAndErr.class) + void interceptsStream(String configParam, Supplier printStreamSupplier, String reportKey) { + DemoHierarchicalTestEngine engine = new DemoHierarchicalTestEngine("engine"); + TestDescriptor test = engine.addTest("test", () -> printStreamSupplier.get().print("4567890")); + TestExecutionListener listener = mock(TestExecutionListener.class); + doAnswer(invocation -> { + TestIdentifier testIdentifier = invocation.getArgument(0); + if (testIdentifier.getUniqueId().equals(test.getUniqueId().toString())) { + printStreamSupplier.get().print("123"); + } + return null; + }).when(listener).executionStarted(any()); + + DefaultLauncher launcher = createLauncher(engine); + LauncherDiscoveryRequest discoveryRequest = request()// + .selectors(selectUniqueId(test.getUniqueId()))// + .configurationParameter(configParam, String.valueOf(true))// + .configurationParameter(LauncherConstants.CAPTURE_MAX_BUFFER_PROPERTY_NAME, String.valueOf(5))// + .build(); + launcher.execute(discoveryRequest, listener); + + ArgumentCaptor testPlanArgumentCaptor = ArgumentCaptor.forClass(TestPlan.class); + InOrder inOrder = inOrder(listener); + inOrder.verify(listener).testPlanExecutionStarted(testPlanArgumentCaptor.capture()); + TestPlan testPlan = testPlanArgumentCaptor.getValue(); + TestIdentifier testIdentifier = testPlan.getTestIdentifier(test.getUniqueId().toString()); + + ArgumentCaptor reportEntryArgumentCaptor = ArgumentCaptor.forClass(ReportEntry.class); + inOrder.verify(listener).reportingEntryPublished(same(testIdentifier), reportEntryArgumentCaptor.capture()); + inOrder.verify(listener).executionFinished(testIdentifier, successful()); + ReportEntry reportEntry = reportEntryArgumentCaptor.getValue(); + + assertThat(reportEntry.getKeyValuePairs()).containsExactly(entry(reportKey, "12345")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("systemStreams") + @ExtendWith(HiddenSystemOutAndErr.class) + void doesNotInterceptStreamWhenAlreadyBeingIntercepted(String configParam, + Supplier printStreamSupplier) { + DemoHierarchicalTestEngine engine = new DemoHierarchicalTestEngine("engine"); + TestDescriptor test = engine.addTest("test", () -> printStreamSupplier.get().print("1234567890")); + + assertThat(StreamInterceptor.registerStdout(1)).isPresent(); + assertThat(StreamInterceptor.registerStderr(1)).isPresent(); + + DefaultLauncher launcher = createLauncher(engine); + LauncherDiscoveryRequest discoveryRequest = request()// + .selectors(selectUniqueId(test.getUniqueId()))// + .configurationParameter(configParam, String.valueOf(true))// + .build(); + TestExecutionListener listener = mock(TestExecutionListener.class); + launcher.execute(discoveryRequest, listener); + + verify(listener, never()).reportingEntryPublished(any(), any()); + } + + private static Stream systemStreams() { + return Stream.of(// + streamType(CAPTURE_STDOUT_PROPERTY_NAME, () -> System.out, STDOUT_REPORT_ENTRY_KEY), // + streamType(CAPTURE_STDERR_PROPERTY_NAME, () -> System.err, STDERR_REPORT_ENTRY_KEY)); + } + + private static Arguments streamType(String configParam, Supplier printStreamSupplier, + String reportKey) { + return arguments(configParam, printStreamSupplier, reportKey); + } + + static class HiddenSystemOutAndErr implements BeforeTestExecutionCallback, AfterTestExecutionCallback { + + private static final Namespace NAMESPACE = Namespace.create(HiddenSystemOutAndErr.class); + + @Override + public void beforeTestExecution(ExtensionContext context) { + ExtensionContext.Store store = context.getStore(NAMESPACE); + store.put("out", System.out); + store.put("err", System.err); + System.setOut(new PrintStream(new ByteArrayOutputStream())); + System.setErr(new PrintStream(new ByteArrayOutputStream())); + } + + @Override + public void afterTestExecution(ExtensionContext context) { + ExtensionContext.Store store = context.getStore(NAMESPACE); + System.setOut(store.get("out", PrintStream.class)); + System.setErr(store.get("err", PrintStream.class)); + } + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptorTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptorTests.java new file mode 100644 index 000000000000..2ed08895b0ce --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptorTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; + +/** + * @since 1.3 + */ +class StreamInterceptorTests { + + private ByteArrayOutputStream originalOut = new ByteArrayOutputStream(); + private PrintStream targetStream = new PrintStream(originalOut); + + @Test + void interceptsWriteOperationsToStreamPerThread() { + StreamInterceptor streamInterceptor = StreamInterceptor.register(targetStream, + newStream -> this.targetStream = newStream, 3).orElseThrow(RuntimeException::new); + // @formatter:off + IntStream.range(0, 1000) + .parallel() + .peek(i -> targetStream.println(i)) + .mapToObj(String::valueOf) + .peek(i -> streamInterceptor.capture()) + .peek(i -> targetStream.println(i)) + .forEach(i -> assertEquals(i, streamInterceptor.consume().trim())); + // @formatter:on + } + + @Test + void unregisterRestoresOriginalStream() { + PrintStream originalStream = targetStream; + + StreamInterceptor streamInterceptor = StreamInterceptor.register(targetStream, + newStream -> this.targetStream = newStream, 3).orElseThrow(RuntimeException::new); + assertSame(streamInterceptor, targetStream); + + streamInterceptor.unregister(); + assertSame(originalStream, targetStream); + } + + @Test + void writeForwardsOperationsToOriginalStream() throws IOException { + PrintStream originalStream = targetStream; + + StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream, 2).orElseThrow( + RuntimeException::new); + assertNotSame(originalStream, targetStream); + + targetStream.write('a'); + targetStream.write("b".getBytes()); + targetStream.write("c".getBytes(), 0, 1); + assertEquals("abc", originalOut.toString()); + } + + @Test + void handlesNestedCaptures() { + StreamInterceptor streamInterceptor = StreamInterceptor.register(targetStream, + newStream -> this.targetStream = newStream, 100).orElseThrow(RuntimeException::new); + + String outermost, inner, innermost; + + streamInterceptor.capture(); + streamInterceptor.print("before outermost - "); + { + streamInterceptor.capture(); + streamInterceptor.print("before inner - "); + { + streamInterceptor.capture(); + streamInterceptor.print("innermost"); + innermost = streamInterceptor.consume(); + } + streamInterceptor.print("after inner"); + inner = streamInterceptor.consume(); + } + streamInterceptor.print("after outermost"); + outermost = streamInterceptor.consume(); + + assertAll(// + () -> assertEquals("before outermost - after outermost", outermost), // + () -> assertEquals("before inner - after inner", inner), // + () -> assertEquals("innermost", innermost) // + ); + } +} diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt index 35d3b049cea1..7e4e37bd8ff8 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt @@ -6,4 +6,5 @@ contains org.junit.jupiter.api contains org.junit.jupiter.api.condition contains org.junit.jupiter.api.extension contains org.junit.jupiter.api.function +contains org.junit.jupiter.api.parallel diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt index ca72d78faeb2..c4d5eaf137d6 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt @@ -5,6 +5,7 @@ requires java.base mandated contains org.junit.platform.engine contains org.junit.platform.engine.discovery contains org.junit.platform.engine.reporting +contains org.junit.platform.engine.support.config contains org.junit.platform.engine.support.descriptor contains org.junit.platform.engine.support.filter contains org.junit.platform.engine.support.hierarchical