Skip to content

Commit f6d5e3f

Browse files
committed
Re-Implemented junit 5 batching
- Includes fixed cucumber tag expressions dependency
1 parent 4cd1c0b commit f6d5e3f

File tree

57 files changed

+905
-76
lines changed

Some content is hidden

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

57 files changed

+905
-76
lines changed

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<hamcrest.version>2.2</hamcrest.version>
4949
<slf4j.version>2.0.16</slf4j.version>
5050
<cucumber.version>7.20.1</cucumber.version>
51+
<cucumber.tag-expressions.version>6.1.1</cucumber.tag-expressions.version>
5152
<gson.version>2.11.0</gson.version>
5253
<junit5.version>5.11.1</junit5.version>
5354
<mockito.version>3.3.3</mockito.version>

serenity-cucumber/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,11 @@
212212
<version>${spring.version}</version>
213213
<scope>test</scope>
214214
</dependency>
215+
<dependency>
216+
<groupId>net.serenity-bdd</groupId>
217+
<artifactId>serenity-junit5</artifactId>
218+
<version>${project.version}</version>
219+
<scope>compile</scope>
220+
</dependency>
215221
</dependencies>
216222
</project>

serenity-cucumber/src/main/java/io/cucumber/junit/CucumberSerenityBaseRunner.java

+18-20
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
package io.cucumber.junit;
22

3-
import static io.cucumber.core.runtime.SynchronizedEventBus.synchronize;
4-
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
5-
import static java.util.Arrays.stream;
6-
import static java.util.stream.Collectors.groupingBy;
7-
import static java.util.stream.Collectors.toList;
8-
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_COUNT;
9-
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_NUMBER;
10-
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_COUNT;
11-
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_NUMBER;
123
import io.cucumber.core.eventbus.EventBus;
134
import io.cucumber.core.feature.FeatureParser;
145
import io.cucumber.core.filter.Filters;
@@ -20,25 +11,17 @@
2011
import io.cucumber.core.resource.ClassLoaders;
2112
import io.cucumber.core.runtime.*;
2213
import io.cucumber.plugin.Plugin;
23-
import io.cucumber.tagexpressions.Expression;
24-
import java.net.URI;
25-
import java.time.Clock;
26-
import java.util.*;
27-
import java.util.concurrent.atomic.AtomicInteger;
28-
import java.util.function.Function;
29-
import java.util.function.Predicate;
30-
import java.util.function.Supplier;
31-
import net.serenitybdd.cucumber.SerenityOptions;
3214
import net.serenitybdd.cucumber.suiteslicing.CucumberSuiteSlicer;
3315
import net.serenitybdd.cucumber.suiteslicing.ScenarioFilter;
3416
import net.serenitybdd.cucumber.suiteslicing.TestStatistics;
3517
import net.serenitybdd.cucumber.suiteslicing.WeightedCucumberScenarios;
18+
import io.cucumber.tagexpressions.Expression;
19+
import net.serenitybdd.cucumber.SerenityOptions;
3620
import net.serenitybdd.cucumber.util.PathUtils;
3721
import net.serenitybdd.cucumber.util.Splitter;
22+
import net.thucydides.core.steps.StepEventBus;
3823
import net.thucydides.model.ThucydidesSystemProperty;
3924
import net.thucydides.model.environment.SystemEnvironmentVariables;
40-
import net.thucydides.core.steps.StepEventBus;
41-
import net.thucydides.model.requirements.reports.MultipleSourceRequirmentsOutcomeFactory;
4225
import net.thucydides.model.util.EnvironmentVariables;
4326
import org.junit.runner.Description;
4427
import org.junit.runner.manipulation.NoTestsRemainException;
@@ -50,6 +33,21 @@
5033
import org.slf4j.Logger;
5134
import org.slf4j.LoggerFactory;
5235

36+
import java.net.URI;
37+
import java.time.Clock;
38+
import java.util.*;
39+
import java.util.concurrent.atomic.AtomicInteger;
40+
import java.util.function.Function;
41+
import java.util.function.Predicate;
42+
import java.util.function.Supplier;
43+
44+
import static io.cucumber.core.runtime.SynchronizedEventBus.synchronize;
45+
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
46+
import static java.util.Arrays.stream;
47+
import static java.util.stream.Collectors.groupingBy;
48+
import static java.util.stream.Collectors.toList;
49+
import static net.thucydides.model.ThucydidesSystemProperty.*;
50+
5351
public class CucumberSerenityBaseRunner extends ParentRunner<ParentRunner<?>> {
5452

5553
private static final Logger LOGGER = LoggerFactory.getLogger(CucumberSerenityBaseRunner.class);

serenity-cucumber/src/test/java/net/serenitybdd/cucumber/suiteslicing/SlicedTestRunner.java

-29
This file was deleted.

serenity-junit5/pom.xml

+23-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,18 @@
3939
<version>${project.version}</version>
4040
<scope>compile</scope>
4141
</dependency>
42-
4342
<!-- TEST DEPENDENCIES -->
43+
<dependency>
44+
<groupId>io.cucumber</groupId>
45+
<artifactId>cucumber-junit-platform-engine</artifactId>
46+
<scope>compile</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>io.cucumber</groupId>
50+
<artifactId>tag-expressions</artifactId>
51+
<version>${cucumber.tag-expressions.version}</version>
52+
<scope>compile</scope>
53+
</dependency>
4454
<dependency>
4555
<groupId>org.junit.jupiter</groupId>
4656
<artifactId>junit-jupiter-api</artifactId>
@@ -82,7 +92,7 @@
8292
<dependency>
8393
<groupId>junit</groupId>
8494
<artifactId>junit</artifactId>
85-
<scope>test</scope>
95+
<scope>compile</scope>
8696
</dependency>
8797
<dependency>
8898
<groupId>org.springframework</groupId>
@@ -108,5 +118,16 @@
108118
<version>${assertj.version}</version>
109119
<scope>test</scope>
110120
</dependency>
121+
<dependency>
122+
<groupId>org.apache.commons</groupId>
123+
<artifactId>commons-csv</artifactId>
124+
<version>${commons.csv.version}</version>
125+
<scope>compile</scope>
126+
</dependency>
127+
<dependency>
128+
<groupId>org.junit.platform</groupId>
129+
<artifactId>junit-platform-suite-api</artifactId>
130+
<scope>test</scope>
131+
</dependency>
111132
</dependencies>
112133
</project>

serenity-cucumber/src/main/java/io/cucumber/gherkin/CucumberScenarioLoader.java serenity-junit5/src/main/java/io/cucumber/gherkin/CucumberScenarioLoader.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import io.cucumber.core.feature.Options;
66
import io.cucumber.core.runtime.FeaturePathFeatureSupplier;
77
import io.cucumber.messages.types.*;
8+
import net.serenitybdd.cucumber.utils.PathUtils;
89
import net.serenitybdd.cucumber.suiteslicing.TestStatistics;
910
import net.serenitybdd.cucumber.suiteslicing.WeightedCucumberScenario;
1011
import net.serenitybdd.cucumber.suiteslicing.WeightedCucumberScenarios;
11-
import net.serenitybdd.cucumber.util.PathUtils;
1212
import org.slf4j.Logger;
1313
import org.slf4j.LoggerFactory;
1414

@@ -85,12 +85,12 @@ private Function<Feature, List<WeightedCucumberScenario>> getScenarios() {
8585
.filter(child -> child.getScenario() != null && child.getScenario().isPresent())
8686
.map(FeatureChild::getScenario)
8787
.map(scenarioDefinition -> new WeightedCucumberScenario(
88-
PathUtils.getAsFile(mapsForFeatures.get(cucumberFeature)).getName(),
89-
cucumberFeature.getName(),
90-
scenarioDefinition.get().getName(),
91-
scenarioWeightFor(cucumberFeature, scenarioDefinition.get()),
92-
tagsFor(cucumberFeature, scenarioDefinition.get()),
93-
scenarioCountFor(scenarioDefinition.get())))
88+
PathUtils.getAsFile(mapsForFeatures.get(cucumberFeature)).getName(),
89+
cucumberFeature.getName(),
90+
scenarioDefinition.get().getName(),
91+
scenarioWeightFor(cucumberFeature, scenarioDefinition.get()),
92+
tagsFor(cucumberFeature, scenarioDefinition.get()),
93+
scenarioCountFor(scenarioDefinition.get())))
9494
.collect(toList());
9595
} catch (Throwable e) {
9696
throw new IllegalStateException(String.format("Could not extract scenarios from %s", mapsForFeatures.get(cucumberFeature)), e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package io.cucumber.junit.platform.engine;
2+
3+
import net.thucydides.model.environment.SystemEnvironmentVariables;
4+
import net.thucydides.model.util.EnvironmentVariables;
5+
6+
import org.junit.jupiter.engine.config.DefaultJupiterConfiguration;
7+
import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor;
8+
import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver;
9+
import org.junit.platform.engine.ConfigurationParameters;
10+
import org.junit.platform.engine.EngineDiscoveryRequest;
11+
import org.junit.platform.engine.ExecutionRequest;
12+
import org.junit.platform.engine.TestDescriptor;
13+
import org.junit.platform.engine.UniqueId;
14+
import org.junit.platform.engine.support.config.PrefixedConfigurationParameters;
15+
import org.junit.platform.engine.support.hierarchical.ForkJoinPoolHierarchicalTestExecutorService;
16+
import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine;
17+
import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import java.util.ArrayList;
22+
import java.util.Comparator;
23+
import java.util.List;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
26+
import java.util.stream.IntStream;
27+
import java.util.stream.Stream;
28+
29+
import static io.cucumber.core.options.Constants.FILTER_TAGS_PROPERTY_NAME;
30+
import static io.cucumber.junit.platform.engine.Constants.PARALLEL_CONFIG_PREFIX;
31+
import static io.cucumber.junit.platform.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME;
32+
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_COUNT;
33+
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_NUMBER;
34+
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_COUNT;
35+
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_NUMBER;
36+
37+
38+
public final class CucumberBatchTestEngine extends HierarchicalTestEngine<CucumberEngineExecutionContext> {
39+
40+
static final Logger LOGGER = LoggerFactory.getLogger(CucumberBatchTestEngine.class);
41+
42+
@Override
43+
public String getId() {
44+
return "cucumber-batch";
45+
}
46+
47+
@Override
48+
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
49+
CucumberEngineDescriptor engineDescriptor = new CucumberEngineDescriptor(uniqueId);
50+
DefaultJupiterConfiguration jupiterConfiguration = new DefaultJupiterConfiguration(null);
51+
JupiterEngineDescriptor dd = new JupiterEngineDescriptor(uniqueId, jupiterConfiguration);
52+
new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, dd);
53+
return engineDescriptor;
54+
}
55+
56+
@Override
57+
protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest request) {
58+
ConfigurationParameters config = request.getConfigurationParameters();
59+
if (config.getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME).orElse(false)) {
60+
return new ForkJoinPoolHierarchicalTestExecutorService(
61+
new PrefixedConfigurationParameters(config, PARALLEL_CONFIG_PREFIX));
62+
}
63+
64+
if (!request.getRootTestDescriptor().getChildren().isEmpty()) {
65+
processRequestIfBatched(request);
66+
}
67+
68+
return super.createExecutorService(request);
69+
}
70+
71+
static void processRequestIfBatched(ExecutionRequest request) {
72+
//populate list
73+
String tagFilter = request.getConfigurationParameters().get(FILTER_TAGS_PROPERTY_NAME)
74+
.orElse(System.getProperty(FILTER_TAGS_PROPERTY_NAME));
75+
List<WeightedTest> scenarioList = request.getRootTestDescriptor().getChildren().stream()
76+
.map(TestDescriptor::getChildren)
77+
.flatMap(Set::stream)
78+
.map(WeightedTest::new)
79+
.collect(Collectors.toList());
80+
int total = scenarioList.size();
81+
List<WeightedTest> tagFilteredScenarioList = scenarioList.stream()
82+
.filter(scenario -> scenario.isTagMatchingFilter(tagFilter))
83+
.collect(Collectors.toList());
84+
LOGGER.info("Found {} scenarios in classpath, {} match(es) tag filter {}", total, tagFilteredScenarioList.size(), tagFilter);
85+
86+
EnvironmentVariables envs = SystemEnvironmentVariables.currentEnvironmentVariables();
87+
int batchCount = envs.getPropertyAsInteger(SERENITY_BATCH_COUNT, 1);
88+
int batchNumber = envs.getPropertyAsInteger(SERENITY_BATCH_NUMBER, 1);
89+
int forkCount = envs.getPropertyAsInteger(SERENITY_FORK_COUNT, 1);
90+
int forkNumber = envs.getPropertyAsInteger(SERENITY_FORK_NUMBER, 1);
91+
92+
LOGGER.info("Parameters: \n{}", request.getConfigurationParameters());
93+
LOGGER.info("Running partitioning for batch {} of {} and fork {} of {}", batchNumber,
94+
batchCount, forkNumber, forkCount);
95+
96+
List<WeightedTest> batch = getPartition(tagFilteredScenarioList, batchCount, batchNumber);
97+
List<WeightedTest> testToRun = getPartition(batch, forkCount, forkNumber);
98+
99+
//prune and keep only test to run
100+
scenarioList.removeAll(testToRun);
101+
scenarioList.forEach(WeightedTest::removeFromHierarchy);
102+
103+
LOGGER.info("Running {} of {} scenarios", testToRun.size(), total);
104+
LOGGER.info("Test to run: {}", testToRun);
105+
LOGGER.info("Root test descriptor has {} feature(s)",
106+
request.getRootTestDescriptor().getChildren().size());
107+
}
108+
109+
@Override
110+
protected CucumberEngineExecutionContext createExecutionContext(ExecutionRequest request) {
111+
return new CucumberEngineExecutionContext(request.getConfigurationParameters());
112+
}
113+
114+
static List<WeightedTest> getPartition(List<WeightedTest> list, int partitions, int index) {
115+
if (partitions == 1 && index == 1) {
116+
return new ArrayList<>(list);
117+
}
118+
return getPartitionedTests(list, partitions).get(index - 1);
119+
}
120+
121+
static List<List<WeightedTest>> getPartitionedTests(List<WeightedTest> list, int partitions) {
122+
List<List<WeightedTest>> result = Stream.generate(ArrayList<WeightedTest>::new)
123+
.limit(partitions)
124+
.collect(Collectors.toList());
125+
126+
//sort all scenarios from large to small
127+
list.sort(Comparator.comparing(WeightedTest::getWeight).reversed());
128+
int[] weights = new int[partitions];
129+
130+
for (WeightedTest test : list) {
131+
int minPartition = getMinPartition(weights);
132+
result.get(minPartition).add(test);
133+
weights[minPartition] += test.getWeight();
134+
}
135+
136+
for (int i = 0; i < result.size(); i++) {
137+
LOGGER.info("{} of {}, weight = {}", i + 1, partitions,
138+
result.get(i).stream().mapToInt(WeightedTest::getWeight).sum());
139+
LOGGER.info(print(result.get(i)));
140+
}
141+
return result;
142+
}
143+
144+
private static String print(List<WeightedTest> list) {
145+
return list.stream().map(WeightedTest::toString).collect(Collectors.joining("\n"));
146+
}
147+
148+
private static int getMinPartition(int[] weights) {
149+
return IntStream.range(0, weights.length)
150+
.boxed()
151+
.min(Comparator.comparingInt(i -> weights[i]))
152+
.orElse(-1);
153+
}
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.cucumber.junit.platform.engine;
2+
3+
import java.math.BigDecimal;
4+
import java.net.URI;
5+
import java.util.List;
6+
import net.thucydides.model.environment.SystemEnvironmentVariables;
7+
import org.junit.platform.engine.TestDescriptor;
8+
import org.junit.platform.engine.support.descriptor.ClasspathResourceSource;
9+
10+
import net.serenitybdd.cucumber.suiteslicing.TestStatistics;
11+
12+
13+
class TestWeightCalculator {
14+
15+
private static TestStatistics statistics;
16+
17+
static int calculateWeight(TestDescriptor descriptor) {
18+
return getEstimatedTestDuration(descriptor).intValue();
19+
}
20+
21+
private static BigDecimal getEstimatedTestDuration(TestDescriptor descriptor) {
22+
if (statistics == null) {
23+
statistics = TestStatistics.from(SystemEnvironmentVariables.currentEnvironmentVariables(),
24+
List.of(URI.create("classpath:" + getTopFeatureDirectory(descriptor))));
25+
}
26+
String featureName = descriptor.getParent().map(TestDescriptor::getDisplayName).orElseThrow();
27+
String scenarioName = descriptor.getDisplayName();
28+
return statistics.scenarioWeightFor(featureName, scenarioName);
29+
}
30+
31+
private static String getTopFeatureDirectory(TestDescriptor descriptor) {
32+
ClasspathResourceSource resource = (ClasspathResourceSource) descriptor.getSource().orElseThrow();
33+
return resource.getClasspathResourceName().split("/")[0];
34+
}
35+
}
36+

0 commit comments

Comments
 (0)