Skip to content
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

Implemented junit 5 batching #3569

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading