-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Conversation
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
Result of pair programming session with @leonard84. Issue: #60
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
Co-authored-by: Leonard Brünings <[email protected]>
Co-authored-by: Leonard Brünings <[email protected]>
# Conflicts: # junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java
Co-authored-by: Leonard Brünings <[email protected]>
Co-authored-by: Leonard Brünings <[email protected]>
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
|
||
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` |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
. 😉
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs precondition checks
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
. 😉
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
. 😉
There was a problem hiding this comment.
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.
I just pushed two additional commits that address most of the feedback. Only remaining issue is the name of |
I approved the changes requested thus far and agree that we still need to decide on the name of 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
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>>
?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. 😉
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returnsSingleLockForExclusiveResourcWithBothLockModes
+ UsingStrongestLockMode
There was a problem hiding this comment.
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.
Team Decision: Use |
Wohoo! |
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]>
👏 |
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]>
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]>
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
@API
annotations