Skip to content

Commit

Permalink
Re-Implemented junit 5 batching (#3569)
Browse files Browse the repository at this point in the history
- Includes fixed cucumber tag expressions dependency
  • Loading branch information
nbarrett authored Nov 8, 2024
1 parent 782bc23 commit f0ee557
Show file tree
Hide file tree
Showing 57 changed files with 905 additions and 76 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<hamcrest.version>2.2</hamcrest.version>
<slf4j.version>2.0.16</slf4j.version>
<cucumber.version>7.20.1</cucumber.version>
<cucumber.tag-expressions.version>6.1.1</cucumber.tag-expressions.version>
<gson.version>2.11.0</gson.version>
<junit5.version>5.11.1</junit5.version>
<mockito.version>3.3.3</mockito.version>
Expand Down
6 changes: 6 additions & 0 deletions serenity-cucumber/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,11 @@
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
package io.cucumber.junit;

import static io.cucumber.core.runtime.SynchronizedEventBus.synchronize;
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_COUNT;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_NUMBER;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_COUNT;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_NUMBER;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.feature.FeatureParser;
import io.cucumber.core.filter.Filters;
Expand All @@ -20,25 +11,17 @@
import io.cucumber.core.resource.ClassLoaders;
import io.cucumber.core.runtime.*;
import io.cucumber.plugin.Plugin;
import io.cucumber.tagexpressions.Expression;
import java.net.URI;
import java.time.Clock;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import net.serenitybdd.cucumber.SerenityOptions;
import net.serenitybdd.cucumber.suiteslicing.CucumberSuiteSlicer;
import net.serenitybdd.cucumber.suiteslicing.ScenarioFilter;
import net.serenitybdd.cucumber.suiteslicing.TestStatistics;
import net.serenitybdd.cucumber.suiteslicing.WeightedCucumberScenarios;
import io.cucumber.tagexpressions.Expression;
import net.serenitybdd.cucumber.SerenityOptions;
import net.serenitybdd.cucumber.util.PathUtils;
import net.serenitybdd.cucumber.util.Splitter;
import net.thucydides.core.steps.StepEventBus;
import net.thucydides.model.ThucydidesSystemProperty;
import net.thucydides.model.environment.SystemEnvironmentVariables;
import net.thucydides.core.steps.StepEventBus;
import net.thucydides.model.requirements.reports.MultipleSourceRequirmentsOutcomeFactory;
import net.thucydides.model.util.EnvironmentVariables;
import org.junit.runner.Description;
import org.junit.runner.manipulation.NoTestsRemainException;
Expand All @@ -50,6 +33,21 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.time.Clock;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

import static io.cucumber.core.runtime.SynchronizedEventBus.synchronize;
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static net.thucydides.model.ThucydidesSystemProperty.*;

public class CucumberSerenityBaseRunner extends ParentRunner<ParentRunner<?>> {

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

This file was deleted.

25 changes: 23 additions & 2 deletions serenity-junit5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,18 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>

<!-- TEST DEPENDENCIES -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>tag-expressions</artifactId>
<version>${cucumber.tag-expressions.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down Expand Up @@ -82,7 +92,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
Expand All @@ -108,5 +118,16 @@
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>${commons.csv.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import io.cucumber.core.feature.Options;
import io.cucumber.core.runtime.FeaturePathFeatureSupplier;
import io.cucumber.messages.types.*;
import net.serenitybdd.cucumber.utils.PathUtils;
import net.serenitybdd.cucumber.suiteslicing.TestStatistics;
import net.serenitybdd.cucumber.suiteslicing.WeightedCucumberScenario;
import net.serenitybdd.cucumber.suiteslicing.WeightedCucumberScenarios;
import net.serenitybdd.cucumber.util.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -85,12 +85,12 @@ private Function<Feature, List<WeightedCucumberScenario>> getScenarios() {
.filter(child -> child.getScenario() != null && child.getScenario().isPresent())
.map(FeatureChild::getScenario)
.map(scenarioDefinition -> new WeightedCucumberScenario(
PathUtils.getAsFile(mapsForFeatures.get(cucumberFeature)).getName(),
cucumberFeature.getName(),
scenarioDefinition.get().getName(),
scenarioWeightFor(cucumberFeature, scenarioDefinition.get()),
tagsFor(cucumberFeature, scenarioDefinition.get()),
scenarioCountFor(scenarioDefinition.get())))
PathUtils.getAsFile(mapsForFeatures.get(cucumberFeature)).getName(),
cucumberFeature.getName(),
scenarioDefinition.get().getName(),
scenarioWeightFor(cucumberFeature, scenarioDefinition.get()),
tagsFor(cucumberFeature, scenarioDefinition.get()),
scenarioCountFor(scenarioDefinition.get())))
.collect(toList());
} catch (Throwable e) {
throw new IllegalStateException(String.format("Could not extract scenarios from %s", mapsForFeatures.get(cucumberFeature)), e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package io.cucumber.junit.platform.engine;

import net.thucydides.model.environment.SystemEnvironmentVariables;
import net.thucydides.model.util.EnvironmentVariables;

import org.junit.jupiter.engine.config.DefaultJupiterConfiguration;
import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor;
import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static io.cucumber.core.options.Constants.FILTER_TAGS_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.PARALLEL_CONFIG_PREFIX;
import static io.cucumber.junit.platform.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_COUNT;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_BATCH_NUMBER;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_COUNT;
import static net.thucydides.model.ThucydidesSystemProperty.SERENITY_FORK_NUMBER;


public final class CucumberBatchTestEngine extends HierarchicalTestEngine<CucumberEngineExecutionContext> {

static final Logger LOGGER = LoggerFactory.getLogger(CucumberBatchTestEngine.class);

@Override
public String getId() {
return "cucumber-batch";
}

@Override
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
CucumberEngineDescriptor engineDescriptor = new CucumberEngineDescriptor(uniqueId);
DefaultJupiterConfiguration jupiterConfiguration = new DefaultJupiterConfiguration(null);
JupiterEngineDescriptor dd = new JupiterEngineDescriptor(uniqueId, jupiterConfiguration);
new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, dd);
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));
}

if (!request.getRootTestDescriptor().getChildren().isEmpty()) {
processRequestIfBatched(request);
}

return super.createExecutorService(request);
}

static void processRequestIfBatched(ExecutionRequest request) {
//populate list
String tagFilter = request.getConfigurationParameters().get(FILTER_TAGS_PROPERTY_NAME)
.orElse(System.getProperty(FILTER_TAGS_PROPERTY_NAME));
List<WeightedTest> scenarioList = request.getRootTestDescriptor().getChildren().stream()
.map(TestDescriptor::getChildren)
.flatMap(Set::stream)
.map(WeightedTest::new)
.collect(Collectors.toList());
int total = scenarioList.size();
List<WeightedTest> tagFilteredScenarioList = scenarioList.stream()
.filter(scenario -> scenario.isTagMatchingFilter(tagFilter))
.collect(Collectors.toList());
LOGGER.info("Found {} scenarios in classpath, {} match(es) tag filter {}", total, tagFilteredScenarioList.size(), tagFilter);

EnvironmentVariables envs = SystemEnvironmentVariables.currentEnvironmentVariables();
int batchCount = envs.getPropertyAsInteger(SERENITY_BATCH_COUNT, 1);
int batchNumber = envs.getPropertyAsInteger(SERENITY_BATCH_NUMBER, 1);
int forkCount = envs.getPropertyAsInteger(SERENITY_FORK_COUNT, 1);
int forkNumber = envs.getPropertyAsInteger(SERENITY_FORK_NUMBER, 1);

LOGGER.info("Parameters: \n{}", request.getConfigurationParameters());
LOGGER.info("Running partitioning for batch {} of {} and fork {} of {}", batchNumber,
batchCount, forkNumber, forkCount);

List<WeightedTest> batch = getPartition(tagFilteredScenarioList, batchCount, batchNumber);
List<WeightedTest> testToRun = getPartition(batch, forkCount, forkNumber);

//prune and keep only test to run
scenarioList.removeAll(testToRun);
scenarioList.forEach(WeightedTest::removeFromHierarchy);

LOGGER.info("Running {} of {} scenarios", testToRun.size(), total);
LOGGER.info("Test to run: {}", testToRun);
LOGGER.info("Root test descriptor has {} feature(s)",
request.getRootTestDescriptor().getChildren().size());
}

@Override
protected CucumberEngineExecutionContext createExecutionContext(ExecutionRequest request) {
return new CucumberEngineExecutionContext(request.getConfigurationParameters());
}

static List<WeightedTest> getPartition(List<WeightedTest> list, int partitions, int index) {
if (partitions == 1 && index == 1) {
return new ArrayList<>(list);
}
return getPartitionedTests(list, partitions).get(index - 1);
}

static List<List<WeightedTest>> getPartitionedTests(List<WeightedTest> list, int partitions) {
List<List<WeightedTest>> result = Stream.generate(ArrayList<WeightedTest>::new)
.limit(partitions)
.collect(Collectors.toList());

//sort all scenarios from large to small
list.sort(Comparator.comparing(WeightedTest::getWeight).reversed());
int[] weights = new int[partitions];

for (WeightedTest test : list) {
int minPartition = getMinPartition(weights);
result.get(minPartition).add(test);
weights[minPartition] += test.getWeight();
}

for (int i = 0; i < result.size(); i++) {
LOGGER.info("{} of {}, weight = {}", i + 1, partitions,
result.get(i).stream().mapToInt(WeightedTest::getWeight).sum());
LOGGER.info(print(result.get(i)));
}
return result;
}

private static String print(List<WeightedTest> list) {
return list.stream().map(WeightedTest::toString).collect(Collectors.joining("\n"));
}

private static int getMinPartition(int[] weights) {
return IntStream.range(0, weights.length)
.boxed()
.min(Comparator.comparingInt(i -> weights[i]))
.orElse(-1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.cucumber.junit.platform.engine;

import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import net.thucydides.model.environment.SystemEnvironmentVariables;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.support.descriptor.ClasspathResourceSource;

import net.serenitybdd.cucumber.suiteslicing.TestStatistics;


class TestWeightCalculator {

private static TestStatistics statistics;

static int calculateWeight(TestDescriptor descriptor) {
return getEstimatedTestDuration(descriptor).intValue();
}

private static BigDecimal getEstimatedTestDuration(TestDescriptor descriptor) {
if (statistics == null) {
statistics = TestStatistics.from(SystemEnvironmentVariables.currentEnvironmentVariables(),
List.of(URI.create("classpath:" + getTopFeatureDirectory(descriptor))));
}
String featureName = descriptor.getParent().map(TestDescriptor::getDisplayName).orElseThrow();
String scenarioName = descriptor.getDisplayName();
return statistics.scenarioWeightFor(featureName, scenarioName);
}

private static String getTopFeatureDirectory(TestDescriptor descriptor) {
ClasspathResourceSource resource = (ClasspathResourceSource) descriptor.getSource().orElseThrow();
return resource.getClasspathResourceName().split("/")[0];
}
}

Loading

0 comments on commit f0ee557

Please sign in to comment.