Skip to content

Commit f840358

Browse files
boaty82mpkorstanje
authored andcommitted
[Core] Support parallel execution of pickles (cucumber#1389)
Adds supports for parallel execution of pickles to cucumber-jvm # Added --threads argument to runtime options Allows users of the CLI to specify the (max) number of threads to be used to run the tests. TestNG/JUnit users should consult their documentation on how to run JUnit/TestNG in parallel. Note that JUnit will only run features in parallel, not scenarios. # Concurrent Events During parallel executing events from the execution of different pickles may interleave. To avoid breaking existing Formatters, these will now receive all test events after the run is complete. Because we are unable to infer whether JUnit/TestNG run in parallel the assumption is that they are and their formatter will always get all events after the run. Formatters that can handle concurrent events can events in real time by implementing the ConcurrentEventListener. # New formatter introduced TimelineFormatter Which produces reports using vsjis.org timeline to highlight which feature was run on which Thread and when.
1 parent 88c5f16 commit f840358

File tree

87 files changed

+3825
-1927
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+3825
-1927
lines changed

core/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@
6464
<scope>test</scope>
6565
</dependency>
6666

67+
<dependency>
68+
<groupId>uk.co.datumedge</groupId>
69+
<artifactId>hamcrest-json</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
73+
<dependency>
74+
<groupId>org.assertj</groupId>
75+
<artifactId>assertj-core</artifactId>
76+
<scope>test</scope>
77+
</dependency>
78+
6779
</dependencies>
6880

6981
<build>
Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,12 @@
11
package cucumber.api.cli;
22

3-
import cucumber.runner.EventBus;
4-
import cucumber.runner.TimeService;
5-
import cucumber.runtime.BackendModuleBackendSupplier;
6-
import cucumber.runtime.ClassFinder;
7-
import cucumber.runtime.ExitStatus;
8-
import cucumber.runtime.FeaturePathFeatureSupplier;
9-
import cucumber.runtime.FeatureSupplier;
10-
import cucumber.runtime.GlueSupplier;
11-
import cucumber.runtime.RunnerSupplier;
12-
import cucumber.runtime.filter.Filters;
13-
import cucumber.runtime.formatter.Plugins;
14-
import cucumber.runtime.filter.RerunFilters;
15-
import cucumber.runtime.ThreadLocalRunnerSupplier;
16-
import cucumber.runtime.RuntimeGlueSupplier;
173
import cucumber.runtime.Runtime;
18-
import cucumber.runtime.RuntimeOptions;
19-
import cucumber.runtime.formatter.PluginFactory;
20-
import cucumber.runtime.io.MultiLoader;
21-
import cucumber.runtime.io.ResourceLoader;
22-
import cucumber.runtime.io.ResourceLoaderClassFinder;
23-
import cucumber.runtime.model.FeatureLoader;
24-
25-
import static java.util.Arrays.asList;
264

275
public class Main {
286

297
public static void main(String[] argv) {
30-
byte exitstatus = run(argv, Thread.currentThread().getContextClassLoader());
31-
System.exit(exitstatus);
8+
byte exitStatus = run(argv, Thread.currentThread().getContextClassLoader());
9+
System.exit(exitStatus);
3210
}
3311

3412
/**
@@ -39,23 +17,13 @@ public static void main(String[] argv) {
3917
* @return 0 if execution was successful, 1 if it was not (test failures)
4018
*/
4119
public static byte run(String[] argv, ClassLoader classLoader) {
42-
RuntimeOptions runtimeOptions = new RuntimeOptions(asList(argv));
4320

44-
ResourceLoader resourceLoader = new MultiLoader(classLoader);
45-
ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
46-
BackendModuleBackendSupplier backendSupplier = new BackendModuleBackendSupplier(resourceLoader, classFinder, runtimeOptions);
47-
EventBus bus = new EventBus(TimeService.SYSTEM);
48-
Plugins plugins = new Plugins(classLoader, new PluginFactory(), bus, runtimeOptions);
49-
ExitStatus exitStatus = new ExitStatus(runtimeOptions);
50-
exitStatus.setEventPublisher(bus);
51-
GlueSupplier glueSupplier = new RuntimeGlueSupplier();
52-
RunnerSupplier runnerSupplier = new ThreadLocalRunnerSupplier(runtimeOptions, bus, backendSupplier, glueSupplier);
53-
FeatureLoader featureLoader = new FeatureLoader(resourceLoader);
54-
FeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(featureLoader, runtimeOptions);
55-
RerunFilters rerunFilters = new RerunFilters(runtimeOptions, featureLoader);
56-
Filters filters = new Filters(runtimeOptions, rerunFilters);
57-
Runtime runtime = new Runtime(plugins, bus, filters, runnerSupplier, featureSupplier);
21+
final Runtime runtime = Runtime.builder()
22+
.withArgs(argv)
23+
.withClassLoader(classLoader)
24+
.build();
25+
5826
runtime.run();
59-
return exitStatus.exitStatus();
27+
return runtime.exitStatus();
6028
}
6129
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cucumber.api.event;
2+
3+
import java.util.Comparator;
4+
import java.util.List;
5+
6+
import static java.util.Arrays.asList;
7+
8+
final class CanonicalEventOrder implements Comparator<Event> {
9+
10+
private static final FixedEventOrderComparator fixedOrder = new FixedEventOrderComparator();
11+
private static final TestCaseEventComparator testCaseOrder = new TestCaseEventComparator();
12+
13+
@Override
14+
public int compare(Event a, Event b) {
15+
int fixedOrder = CanonicalEventOrder.fixedOrder.compare(a, b);
16+
if (fixedOrder != 0) {
17+
return fixedOrder;
18+
}
19+
20+
if (!(a instanceof TestCaseEvent && b instanceof TestCaseEvent)) {
21+
return fixedOrder;
22+
}
23+
24+
return testCaseOrder.compare((TestCaseEvent) a, (TestCaseEvent) b);
25+
}
26+
27+
private static final class FixedEventOrderComparator implements Comparator<Event> {
28+
29+
private final List<Class<? extends Event>> fixedOrder = asList(
30+
(Class<? extends Event>)
31+
TestRunStarted.class,
32+
TestSourceRead.class,
33+
SnippetsSuggestedEvent.class,
34+
TestCaseEvent.class,
35+
TestRunFinished.class
36+
);
37+
38+
@Override
39+
public int compare(final Event a, final Event b) {
40+
return Integer.compare(requireInFixOrder(a.getClass()), requireInFixOrder(b.getClass()));
41+
}
42+
43+
private int requireInFixOrder(Class<? extends Event> o) {
44+
int index = findInFixedOrder(o);
45+
if (index < 0) {
46+
throw new IllegalStateException(o + "was not in " + fixedOrder);
47+
}
48+
return index;
49+
}
50+
51+
private int findInFixedOrder(Class<? extends Event> o) {
52+
for (int i = 0; i < fixedOrder.size(); i++) {
53+
if (fixedOrder.get(i).isAssignableFrom(o)) {
54+
return i;
55+
}
56+
}
57+
return -1;
58+
}
59+
}
60+
61+
private static final class TestCaseEventComparator implements Comparator<TestCaseEvent> {
62+
63+
@Override
64+
public int compare(TestCaseEvent a, TestCaseEvent b) {
65+
int uri = a.testCase.getUri().compareTo(b.testCase.getUri());
66+
if (uri != 0) {
67+
return uri;
68+
}
69+
70+
int line = Integer.compare(a.testCase.getLine(), b.testCase.getLine());
71+
if(line != 0){
72+
return line;
73+
}
74+
75+
return Long.compare(a.getTimeStamp(), b.getTimeStamp());
76+
}
77+
}
78+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package cucumber.api.event;
2+
3+
/**
4+
* When cucumber executes test in parallel or in a framework
5+
* that supports parallel execution (e.g. JUnit or TestNG)
6+
* {@link cucumber.api.TestCase} events from different
7+
* pickles may interleave.
8+
* <p>
9+
* This interface marks an {@link EventListener} as capable of
10+
* understanding interleaved pickle events.
11+
* <p>
12+
* While running tests in parallel cucumber makes the
13+
* following guarantees.
14+
* <p>
15+
* 1. The event publisher is synchronized. Events are not
16+
* handled concurrently.
17+
* <p>
18+
* 2. For test cases executed on different threads the callbacks
19+
* registered on the event publisher will be called by
20+
* different threads. I.e. Thread.currentThread()
21+
* will return different a different thread for two test cases
22+
* executed on a different thread (but not necessarily the
23+
* executing thread).
24+
* <p>
25+
*
26+
* @see Event
27+
*/
28+
public interface ConcurrentEventListener {
29+
30+
/**
31+
* Set the event publisher. The formatter can register event listeners with the publisher.
32+
*
33+
* @param publisher the event publisher
34+
*/
35+
void setEventPublisher(EventPublisher publisher);
36+
37+
}

core/src/main/java/cucumber/api/event/EmbedEvent.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cucumber.api.event;
22

3-
public final class EmbedEvent extends TimeStampedEvent {
3+
import cucumber.api.TestCase;
4+
5+
public final class EmbedEvent extends TestCaseEvent {
46
public final byte[] data;
57
public final String mimeType;
68

7-
public EmbedEvent(Long timeStamp, byte[] data, String mimeType) {
8-
super(timeStamp);
9+
public EmbedEvent(Long timeStamp, TestCase testCase, byte[] data, String mimeType) {
10+
super(timeStamp, testCase);
911
this.data = data;
1012
this.mimeType = mimeType;
1113
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
11
package cucumber.api.event;
22

3+
import java.util.Comparator;
4+
35
public interface Event {
46

7+
/**
8+
* When pickles are executed in parallel or random order
9+
* events can be produced with a partial ordering.
10+
* <p>
11+
* The canonical order is the order in which these events
12+
* would have been generated had cucumber executed these
13+
* pickles is executed in a serial fashion.
14+
* <p>
15+
* In canonical order events are first ordered by type:
16+
* <ol>
17+
* <li>TestRunStarted
18+
* <li>TestSourceRead
19+
* <li>SnippetsSuggestedEvent
20+
* <li>TestCaseEvent
21+
* <li>TestRunFinished
22+
* </ol>
23+
* <p>
24+
* Then TestCaseEvents are ordered by
25+
* <ol>
26+
* <li>uri
27+
* <li>line
28+
* <li>timestamp
29+
* </ol>
30+
*/
31+
Comparator<Event> CANONICAL_ORDER = new CanonicalEventOrder();
32+
533
Long getTimeStamp();
634

735
}

core/src/main/java/cucumber/api/event/EventPublisher.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ public interface EventPublisher {
44

55
/**
66
* Registers an event handler for a specific event.
7-
*
7+
* <p>
88
* The available events types are:
99
* <ul>
10+
* <li>{@link Event} - all events.
1011
* <li>{@link TestRunStarted} - the first event sent.
1112
* <li>{@link TestSourceRead} - sent for each feature file read, contains the feature file source.
1213
* <li>{@link SnippetsSuggestedEvent} - sent for each step that could not be matched to a step definition, contains the raw snippets for the step.
13-
* <li> {@link TestCaseStarted} - sent before starting the execution of a Test Case(/Pickle/Scenario), contains the Test Case
14+
* <li>{@link TestCaseStarted} - sent before starting the execution of a Test Case(/Pickle/Scenario), contains the Test Case
1415
* <li>{@link TestStepStarted} - sent before starting the execution of a Test Step, contains the Test Step
1516
* <li>{@link EmbedEvent} - calling scenario.embed in a hook triggers this event.
1617
* <li>{@link WriteEvent} - calling scenario.write in a hook triggers this event.
@@ -19,10 +20,20 @@ public interface EventPublisher {
1920
* <li>{@link TestRunFinished} - the last event sent.
2021
* </ul>
2122
*
22-
*
2323
* @param eventType the event type for which the handler is being registered
24-
* @param handler the event handler
25-
* @param <T> the event type
24+
* @param handler the event handler
25+
* @param <T> the event type
26+
* @see Event
2627
*/
2728
<T extends Event> void registerHandlerFor(Class<T> eventType, EventHandler<T> handler);
29+
30+
/**
31+
* Unregister an event handler for a specific event
32+
*
33+
* @param eventType the event type for which the handler is being registered
34+
* @param handler the event handler
35+
* @param <T> the event type
36+
*/
37+
<T extends Event> void removeHandlerFor(Class<T> eventType, EventHandler<T> handler);
38+
2839
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package cucumber.api.event;
2+
3+
import cucumber.api.TestCase;
4+
5+
public abstract class TestCaseEvent extends TimeStampedEvent {
6+
7+
final TestCase testCase;
8+
9+
TestCaseEvent(Long timeStamp, TestCase testCase) {
10+
super(timeStamp);
11+
this.testCase = testCase;
12+
}
13+
14+
public TestCase getTestCase() {
15+
return testCase;
16+
}
17+
}

core/src/main/java/cucumber/api/event/TestCaseFinished.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import cucumber.api.Result;
44
import cucumber.api.TestCase;
55

6-
public final class TestCaseFinished extends TimeStampedEvent {
6+
public final class TestCaseFinished extends TestCaseEvent {
77
public final Result result;
88
public final TestCase testCase;
99

1010
public TestCaseFinished(Long timeStamp, TestCase testCase, Result result) {
11-
super(timeStamp);
11+
super(timeStamp, testCase);
1212
this.testCase = testCase;
1313
this.result = result;
1414
}

core/src/main/java/cucumber/api/event/TestCaseStarted.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import cucumber.api.TestCase;
44

5-
public final class TestCaseStarted extends TimeStampedEvent {
5+
public final class TestCaseStarted extends TestCaseEvent {
66
public final TestCase testCase;
77

88
public TestCaseStarted(Long timeStamp, TestCase testCase) {
9-
super(timeStamp);
9+
super(timeStamp, testCase);
1010
this.testCase = testCase;
1111
}
1212

core/src/main/java/cucumber/api/event/TestStepFinished.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import cucumber.api.HookTestStep;
44
import cucumber.api.PickleStepTestStep;
55
import cucumber.api.Result;
6+
import cucumber.api.TestCase;
67
import cucumber.api.TestStep;
78

89
/**
@@ -21,12 +22,12 @@
2122
* @see PickleStepTestStep
2223
* @see HookTestStep
2324
*/
24-
public final class TestStepFinished extends TimeStampedEvent {
25+
public final class TestStepFinished extends TestCaseEvent {
2526
public final TestStep testStep;
2627
public final Result result;
2728

28-
public TestStepFinished(Long timeStamp, TestStep testStep, Result result) {
29-
super(timeStamp);
29+
public TestStepFinished(Long timeStamp, TestCase testCase, TestStep testStep, Result result) {
30+
super(timeStamp, testCase);
3031
this.testStep = testStep;
3132
this.result = result;
3233
}

0 commit comments

Comments
 (0)