Skip to content

Add support for parallel test execution #1461

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 89 commits into from

Conversation

marcphilipp
Copy link
Member

@marcphilipp marcphilipp commented Jun 8, 2018

This PR adds support for parallel test execution (#60). Please refer to the added user guide sections for details.

The majority of this work was done by @leonard84 and me over the course of the last few months. I intend to squash the commits before merging this PR. For reviewing, I've decided to keep them in place so it's easier to see why a change was made.

If you want to see it in action, just execute SlowTests in an IDE (tested in IntelliJ IDEA).


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

marcphilipp and others added 30 commits November 9, 2017 20:49
Thread-safety is achieved by using a synchronized map.

`TreePrinter` now resorts to `Color.NONE` if no color mapping is
specified for the current identifier.
# Conflicts:
#	junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java
Improve CompositeLock to keep track of acquired locks, release only those.
Use lockInterruptably to support clean shutdown while waiting for locks.
Add basic support for ExecutionControl/Mode
# Conflicts:
#	junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java
#	junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExecutionTracker.java
#	platform-tests/src/test/java/org/junit/platform/console/tasks/XmlReportAssertions.java
- Limit max. number of threads
- Set minimum runnable number of threads to parallelism
# Conflicts:
#	junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java
Co-authored-by: Leonard Brünings <[email protected]>
@marcphilipp marcphilipp self-assigned this Jun 8, 2018
@ghost ghost added the status: in progress label Jun 8, 2018
@codecov
Copy link

codecov bot commented Jun 8, 2018

Codecov Report

Merging #1461 into master will increase coverage by 0.17%.
The diff coverage is 91.3%.

Impacted file tree graph

@@             Coverage Diff             @@
##             master   #1461      +/-   ##
===========================================
+ Coverage     91.73%   91.9%   +0.17%     
- Complexity     3214    3487     +273     
===========================================
  Files           297     317      +20     
  Lines          7740    8310     +570     
  Branches        662     723      +61     
===========================================
+ Hits           7100    7637     +537     
- Misses          477     503      +26     
- Partials        163     170       +7
Impacted Files Coverage Δ Complexity Δ
...jupiter/engine/descriptor/ClassTestDescriptor.java 99.15% <100%> (+0.01%) 48 <2> (+2) ⬆️
...r/engine/descriptor/DynamicNodeTestDescriptor.java 100% <100%> (ø) 5 <1> (+1) ⬆️
...tform/engine/support/hierarchical/LockManager.java 100% <100%> (ø) 11 <11> (?)
...junit/platform/engine/ConfigurationParameters.java 100% <100%> (ø) 2 <2> (?)
.../core/StreamInterceptingTestExecutionListener.java 100% <100%> (ø) 16 <16> (?)
...t/platform/console/tasks/TreePrintingListener.java 100% <100%> (ø) 9 <9> (+2) ⬆️
...form/engine/support/hierarchical/ResourceLock.java 100% <100%> (ø) 1 <1> (?)
...rchical/DefaultParallelExecutionConfiguration.java 100% <100%> (ø) 6 <6> (?)
...support/hierarchical/HierarchicalTestExecutor.java 100% <100%> (+4.59%) 3 <3> (-1) ⬇️
.../platform/engine/support/hierarchical/NopLock.java 100% <100%> (ø) 3 <3> (?)
... and 56 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5d1e6ef...61441bb. Read the comment docs.


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.launcher.capture.stdout` and/or `junit.platform.launcher.capture.stderr`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the prefix should be junit.platform.launcher.output.capture or junit.platform.output.capture.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

@@ -30,6 +32,11 @@
this.index = index;
}

@Override
public ExecutionMode getExecutionMode() {
return getParent().map(parent -> ((JupiterTestDescriptor) parent).getExecutionMode()).orElse(CONCURRENT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will Jupiter now default to concurrent execution?

.filter(parent -> parent instanceof Node)
.map(parent -> ((Node<?>) parent).getExecutionMode())
.orElse(ExecutionMode.CONCURRENT));
// @formatter:on
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will Jupiter now default to concurrent execution?

Copy link
Member Author

@marcphilipp marcphilipp Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if the junit.jupiter.execution.parallel.enabled config param is set to true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. In that code it looks like it's defaulting to concurrent, so I'll have to review in greater depth to find where the flag is honored.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just have a look at JupiterTestEngine. 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty. Will do...

*
* @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} or blank
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove "or blank" for the transformer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return transformer.apply(input);
}
catch (Exception ex) {
String message = String.format("Failed to convert configuration parameter with key '%s' for input: %s",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--> Failed to transform configuration parameter with key '%s' and initial value '%s'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* applies the supplied prefix to all queries.
*/
public PrefixedConfigurationParameters(ConfigurationParameters delegate, String prefix) {
this.delegate = delegate;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs precondition checks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Repeatable(UseResources.class)
public @interface UseResource {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't @ResourceLock be a better suited name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly... let's discuss it in the next team call, shall we?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the fact that it is currently implemented via a lock an implementation detail? The idea of @UseResource was so that the tests could declare what resources they use and what guarantees they expect/observe. If we change the implementation to use something else than a Lock then @ResourceLock wouldn't make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is one of those scenarios where an implementation detail leaks to higher levels of the overall abstraction simply because it's part of the high-level use case.

To me, @UseResource is too generic in that it does not immediately provide a mental link to the concurrency model in question. After all, we're talking about executing things in parallel. So there is going to have to be some sort of locking in order to avoid contention -- right?

On the other hand, I understand your desire not to leak implementation details, but I'm still not fond of @UseResource. 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of consistency, IIRC, the only places we use "verbs" for annotation names are for @ExtendWith and @RegisterExtension.

Otherwise, we try to avoid using verbs for annotation names.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly... let's discuss it in the next team call, shall we?

If I'm there... gladly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If people don't like @ResourceLock, I think that @SharedResource would at least be better than @UseResource.

Rationale: the developer is not instructing the framework to use a resource for the test. Rather, the developer is instructing the framework that the test itself uses a shared resource, to which the framework should then synchronize access.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, the demo test class is currently called SharedResourcesDemo, not UseResourcesDemo. 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to agree with @sbrannen that @UseResource is not a good name for this. @ResourceLock would make more sense to me.

Adding the Lock concept does help me to understand the underlying mental(concurrency) model.

When seeing for the first time @UseResource for some reason I imagined that this annotation would be added to some resource ie some critical piece of code eg a method, that would be shared and the framework would orchestrate tests around it. [In retrospect, feeling a bit stupid now for thinking that.]

After reviewing the code I think the important point of this mechanism is the exclusivity of execution of tests, ie these tests characterized as readers can execute at the same time while tests characterized as writers can only run by themselves.

The concept of a lock suggests the above quite well for me.

@marcphilipp
Copy link
Member Author

I just pushed two additional commits that address most of the feedback. Only remaining issue is the name of @UseResource.

@sbrannen
Copy link
Member

I approved the changes requested thus far and agree that we still need to decide on the name of @UseResource.

If I have time, I'll review the internals in greater detail.

false, configuration.getCorePoolSize(), configuration.getMaxPoolSize(),
configuration.getMinimumRunnable(), null, configuration.getKeepAlive(), TimeUnit.SECONDS);
}
catch (Exception e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't catching the all inclusive Exception here also mask actually crashes in the try part of this code, eg NPEs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All ParallelExecutionConfiguration methods return primitive types thus I don't see where a NPE should come from. Am I missing something?

@@ -30,7 +30,7 @@
@API(status = INTERNAL, since = "1.1")
public class LogRecordListener {

private final List<LogRecord> logRecords = new ArrayList<>();
private final List<LogRecord> logRecords = new CopyOnWriteArrayList<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopyOnWriteArrayList is expensive to update, why not use a ThreadLocal<List<LogRecord>>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 650fa52.

for (ExclusiveTask task : nonConcurrentTasks) {
task.compute();
}
for (ExclusiveTask forkedTask : concurrentTasksInReverseOrder) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although inline comments should be used sparingly it might be helpful here to explain a few steps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted three methods in 4bf6044 instead. Please let me know if that's easier to understand now.

@@ -94,4 +109,12 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e

}

interface EagerTestExecutionListener extends TestExecutionListener {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are eager EagerTestExecutionListener? Shouldn't we add ordering instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now this is a one-off internal scenario. I found this easier to understand and less error-prone than adding ordering support.

@@ -26,24 +28,27 @@
*/
class XmlReportAssertions {

private static Validator schemaValidator;
private static AtomicReference<Schema> schema = new AtomicReference<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need an AtomicReference why not just use getSchema as initializer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's surprisingly hard to implement a thread-safe singleton without locking. If you have a better solution, I'm open to change it. 😉

Copy link
Contributor

@jbduncan jbduncan Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did someone say thread-safe singleton? :)

Would Effective Java's recommended solution - a single-instance enum - work in this case? E.g.

enum ClassName implements AppropriateInterface {
    INSTANCE;

    // needed methods go here
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had tried that at some point but it felt clunky. Revisiting it now, it's not actually that bad so I changed it in 135a07d.

@jbduncan Thanks! 🙂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcphilipp You're very welcome! 😃

}

@Test
void releasesAllLocksInReverseOrder() throws Exception {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test for acquire inOrder is missing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in ba7fb76.

void returnsNopLockWithoutExclusiveResources() {
Collection<ExclusiveResource> resources = emptySet();

List<Lock> locks = getLocks(lockManager, resources);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not assert that a NopLock was returned, same goes for the other tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved in 4b40b64.

}

@Test
void returnsSingleLockForExclusiveResourcWithBothLockModes() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returnsSingleLockForExclusiveResourcWithBothLockModes + UsingStrongestLockMode

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to returnsWriteLockForExclusiveResourceWithBothLockModes in 4b40b64.

@marcphilipp
Copy link
Member Author

Team Decision: Use @ResourceLock for M1 and ask the community for feedback.

@ghost ghost removed the status: in progress label Jun 22, 2018
@sormuras
Copy link
Member

Wohoo!

marcphilipp added a commit that referenced this pull request Jun 22, 2018
This commit adds opt-in support for parallel test execution and
capturing output to `System.out` and `System.err`. Both features are
disabled by default but can be enabled and configured using
configuration parameters.

The implementation is based on the Fork/Join Framework and designed to
be reusable by other test engines that extend HierarchicalTestEngine.

The Jupiter API provides annotations to declare which shared resources a
test needs to access and in which way. Moreover, the execution mode of a
test can be influenced.

In addition, a number of TestExecutionListeners have been made
thread-safe.

The documentation subproject is now configured to execute tests in
parallel. All other subprojects will have to wait as Gradle currently
blows up when used with parallel test execution.

Resolves #60. Closes #1461.

Co-authored-by: Leonard Brünings <[email protected]>
Co-authored-by: Christian Stein <[email protected]>
@marcphilipp marcphilipp deleted the experiments/parallel-execution branch June 22, 2018 17:58
@sbrannen
Copy link
Member

👏

Andrei94 pushed a commit to Andrei94/junit5 that referenced this pull request Jun 23, 2018
This commit adds opt-in support for parallel test execution and
capturing output to `System.out` and `System.err`. Both features are
disabled by default but can be enabled and configured using
configuration parameters.

The implementation is based on the Fork/Join Framework and designed to
be reusable by other test engines that extend HierarchicalTestEngine.

The Jupiter API provides annotations to declare which shared resources a
test needs to access and in which way. Moreover, the execution mode of a
test can be influenced.

In addition, a number of TestExecutionListeners have been made
thread-safe.

The documentation subproject is now configured to execute tests in
parallel. All other subprojects will have to wait as Gradle currently
blows up when used with parallel test execution.

Resolves junit-team#60. Closes junit-team#1461.

Co-authored-by: Leonard Brünings <[email protected]>
Co-authored-by: Christian Stein <[email protected]>
dotCipher pushed a commit to dotCipher/junit5 that referenced this pull request Sep 18, 2018
This commit adds opt-in support for parallel test execution and
capturing output to `System.out` and `System.err`. Both features are
disabled by default but can be enabled and configured using
configuration parameters.

The implementation is based on the Fork/Join Framework and designed to
be reusable by other test engines that extend HierarchicalTestEngine.

The Jupiter API provides annotations to declare which shared resources a
test needs to access and in which way. Moreover, the execution mode of a
test can be influenced.

In addition, a number of TestExecutionListeners have been made
thread-safe.

The documentation subproject is now configured to execute tests in
parallel. All other subprojects will have to wait as Gradle currently
blows up when used with parallel test execution.

Resolves junit-team#60. Closes junit-team#1461.

Co-authored-by: Leonard Brünings <[email protected]>
Co-authored-by: Christian Stein <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants