Skip to content

Commit

Permalink
[Core] Support custom UUID generators in test runners (#2926)
Browse files Browse the repository at this point in the history
With #2703 a faster UUID generator was introduced. And while the
configuration options were added, they were not actually used by
`cucumber-junit`, `cucumber-junit-platform-engine` and
`cucumber-testng`.
  • Loading branch information
mpkorstanje authored Sep 26, 2024
1 parent c6c2d07 commit 8029e93
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 40 deletions.
20 changes: 20 additions & 0 deletions .revapi/api-changes.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,26 @@
}
}
],
"7.20.0": [
{
"extension": "revapi.differences",
"id": "intentional-api-changes",
"ignore": true,
"configuration": {
"differences": [
{
"ignore": true,
"code": "java.method.visibilityIncreased",
"old": "method io.cucumber.core.eventbus.UuidGenerator io.cucumber.core.runtime.UuidGeneratorServiceLoader::loadUuidGenerator()",
"new": "method io.cucumber.core.eventbus.UuidGenerator io.cucumber.core.runtime.UuidGeneratorServiceLoader::loadUuidGenerator()",
"oldVisibility": "package",
"newVisibility": "public",
"justification": "Expose internal API to other internal components"
}
]
}
}
],
"internal": [
{
"extension": "revapi.differences",
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- [JUnit Platform Engine] Enable use of custom UUID generators ([#2926](https://github.com/cucumber/cucumber-jvm/pull/2926) M.P. Korstanje)
- [JUnit] Enable use of custom UUID generators ([#2926](https://github.com/cucumber/cucumber-jvm/pull/2926) M.P. Korstanje)
- [TestNG] Enable use of custom UUID generators ([#2926](https://github.com/cucumber/cucumber-jvm/pull/2926) M.P. Korstanje)

### Fixed
- [Core] Use custom UUID generators for hooks ([#2926](https://github.com/cucumber/cucumber-jvm/pull/2926) M.P. Korstanje)


## [7.19.0] - 2024-09-19
### Changed
Expand Down
8 changes: 5 additions & 3 deletions cucumber-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ cucumber.plugin= # comma separated plugin strings.
cucumber.object-factory= # object factory class name.
# example: com.example.MyObjectFactory
cucumber.uuid-generator= # UUID generator class name.
cucumber.uuid-generator # uuid generator class name of a registered service provider.
# default: io.cucumber.core.eventbus.RandomUuidGenerator
# example: com.example.MyUuidGenerator
cucumber.publish.enabled # true or false. default: false
Expand Down Expand Up @@ -89,12 +90,13 @@ Cucumber emits events on an event bus in many cases:
- during the feature file parsing
- when the test scenarios are executed

An event has a UUID. The UUID generator can be configured using the `cucumber.uuid-generator` property:
An event has a UUID. The UUID generator can be configured using the
`cucumber.uuid-generator` property:

| UUID generator | Features | Performance [Millions UUID/second] | Typical usage example |
|-----------------------------------------------------|-----------------------------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| io.cucumber.core.eventbus.RandomUuidGenerator | Thread-safe, collision-free, multi-jvm | ~1 | Reports may be generated on different JVMs at the same time. A typical example would be one suite that tests against Firefox and another against Safari. The exact browser is configured through a property. These are then executed concurrently on different Gitlab runners. |
| io.cucumber.core.eventbus.IncrementingUuidGenerator | Thread-safe, collision-free, single-jvm | ~130 | Reports are generated on a single JVM |
| io.cucumber.core.eventbus.IncrementingUuidGenerator | Thread-safe, collision-free, single-jvm | ~130 | Reports are generated on a single JVM in a single execution of Cucumber. |

The performance gain on real projects depends on the feature size.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
* Thread-safe and collision-free UUID generator for single JVM. This is a
* sequence generator and each instance has its own counter. This generator is
* about 100 times faster than #RandomUuidGenerator.
*
* Properties:
* - thread-safe
* - collision-free in the same classloader
* - almost collision-free in different classloaders / JVMs
* - UUIDs generated using the instances from the same classloader are sortable
*
* UUID version 8 (custom) / variant 2 <a href=
* "https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-8">...</a>
* <!-- @formatter:off -->
* <p>
* Properties: - thread-safe - collision-free in the same classloader - almost
* collision-free in different classloaders / JVMs - UUIDs generated using the
* instances from the same classloader are sortable
* <p>
* <a href=
* "https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-8">UUID
* version 8 (custom) / variant 2 </a>
*
* <pre>
* | 40 bits | 8 bits | 4 bits | 12 bits | 2 bits | 62 bits |
* | -------------------| -------------- | ------- | ------------- | ------- | ------- |
* | LSBs of epoch-time | sessionCounter | version | classloaderId | variant | counter |
* <!-- @formatter:on -->
* </pre>
*/
public class IncrementingUuidGenerator implements UuidGenerator {
/**
Expand Down Expand Up @@ -84,7 +84,7 @@ public class IncrementingUuidGenerator implements UuidGenerator {
* classloaderId which produces about 1% collision rate on the
* classloaderId, and thus can have UUID collision if the epoch-time,
* session counter and counter have the same values).
*
*
* @param classloaderId the new classloaderId (only the least significant 12
* bits are used)
* @see IncrementingUuidGenerator#classloaderId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,25 @@ public void addStepDefinition(StepDefinition stepDefinition) {

@Override
public void addBeforeHook(HookDefinition hookDefinition) {
beforeHooks.add(CoreHookDefinition.create(hookDefinition));
beforeHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
beforeHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
public void addAfterHook(HookDefinition hookDefinition) {
afterHooks.add(CoreHookDefinition.create(hookDefinition));
afterHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
afterHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
public void addBeforeStepHook(HookDefinition hookDefinition) {
beforeStepHooks.add(CoreHookDefinition.create(hookDefinition));
beforeStepHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
beforeStepHooks.sort(HOOK_ORDER_ASCENDING);
}

@Override
public void addAfterStepHook(HookDefinition hookDefinition) {
afterStepHooks.add(CoreHookDefinition.create(hookDefinition));
afterStepHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
afterStepHooks.sort(HOOK_ORDER_ASCENDING);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

Expand All @@ -33,13 +34,13 @@ private CoreHookDefinition(UUID id, HookDefinition delegate) {
}
}

static CoreHookDefinition create(HookDefinition hookDefinition) {
static CoreHookDefinition create(HookDefinition hookDefinition, Supplier<UUID> uuidGenerator) {
// Ideally we would avoid this by keeping the scenario scoped
// glue in a different bucket from the globally scoped glue.
if (hookDefinition instanceof ScenarioScoped) {
return new ScenarioScopedCoreHookDefinition(hookDefinition);
return new ScenarioScopedCoreHookDefinition(uuidGenerator.get(), hookDefinition);
}
return new CoreHookDefinition(UUID.randomUUID(), hookDefinition);
return new CoreHookDefinition(uuidGenerator.get(), hookDefinition);
}

void execute(TestCaseState scenario) {
Expand Down Expand Up @@ -72,8 +73,8 @@ String getTagExpression() {

static class ScenarioScopedCoreHookDefinition extends CoreHookDefinition implements ScenarioScoped {

private ScenarioScopedCoreHookDefinition(HookDefinition delegate) {
super(UUID.randomUUID(), delegate);
private ScenarioScopedCoreHookDefinition(UUID id, HookDefinition delegate) {
super(id, delegate);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public UuidGeneratorServiceLoader(Supplier<ClassLoader> classLoaderSupplier, Opt
this.options = requireNonNull(options);
}

UuidGenerator loadUuidGenerator() {
public UuidGenerator loadUuidGenerator() {
Class<? extends UuidGenerator> objectFactoryClass = options.getUuidGeneratorClass();
ClassLoader classLoader = classLoaderSupplier.get();
ServiceLoader<UuidGenerator> loader = ServiceLoader.load(UuidGenerator.class, classLoader);
Expand Down
4 changes: 4 additions & 0 deletions cucumber-junit-platform-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ cucumber.junit-platform.naming-strategy.long.example-name= # number or pickl
cucumber.plugin= # comma separated plugin strings.
# example: pretty, json:path/to/report.json
cucumber.uuid-generator # uuid generator class name of a registered service provider.
# default: io.cucumber.core.eventbus.RandomUuidGenerator
# example: com.example.MyUuidGenerator
cucumber.object-factory= # object factory class name.
# example: com.example.MyObjectFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
import io.cucumber.core.runtime.ThreadLocalObjectFactorySupplier;
import io.cucumber.core.runtime.ThreadLocalRunnerSupplier;
import io.cucumber.core.runtime.TimeServiceEventBus;
import io.cucumber.core.runtime.UuidGeneratorServiceLoader;
import org.apiguardian.api.API;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.support.hierarchical.EngineExecutionContext;

import java.time.Clock;
import java.util.UUID;
import java.util.function.Supplier;

import static io.cucumber.core.runtime.SynchronizedEventBus.synchronize;
Expand All @@ -48,8 +48,10 @@ CucumberEngineOptions getOptions() {

private CucumberExecutionContext createCucumberExecutionContext() {
Supplier<ClassLoader> classLoader = CucumberEngineExecutionContext.class::getClassLoader;
UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader, options);
EventBus bus = synchronize(
new TimeServiceEventBus(Clock.systemUTC(), uuidGeneratorServiceLoader.loadUuidGenerator()));
ObjectFactoryServiceLoader objectFactoryServiceLoader = new ObjectFactoryServiceLoader(classLoader, options);
EventBus bus = synchronize(new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID));
Plugins plugins = new Plugins(new PluginFactory(), options);
ExitStatus exitStatus = new ExitStatus(options);
plugins.addPlugin(exitStatus);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.cucumber.junit.platform.engine;

import io.cucumber.core.eventbus.UuidGenerator;
import io.cucumber.core.feature.FeatureIdentifier;
import io.cucumber.core.feature.FeatureParser;
import io.cucumber.core.feature.FeatureWithLines;
Expand All @@ -9,6 +10,7 @@
import io.cucumber.core.logging.LoggerFactory;
import io.cucumber.core.resource.ClassLoaders;
import io.cucumber.core.resource.ResourceScanner;
import io.cucumber.core.runtime.UuidGeneratorServiceLoader;
import io.cucumber.junit.platform.engine.NodeDescriptor.ExamplesDescriptor;
import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor;
import io.cucumber.junit.platform.engine.NodeDescriptor.RuleDescriptor;
Expand All @@ -28,8 +30,8 @@

import java.net.URI;
import java.util.List;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static java.util.Comparator.comparing;
Expand All @@ -38,11 +40,7 @@ final class FeatureResolver {

private static final Logger log = LoggerFactory.getLogger(FeatureResolver.class);

private final CachingFeatureParser featureParser = new CachingFeatureParser(new FeatureParser(UUID::randomUUID));
private final ResourceScanner<Feature> featureScanner = new ResourceScanner<>(
ClassLoaders::getDefaultClassLoader,
FeatureIdentifier::isFeature,
featureParser::parseResource);
private final ResourceScanner<Feature> featureScanner;

private final CucumberEngineDescriptor engineDescriptor;
private final Predicate<String> packageFilter;
Expand All @@ -56,7 +54,21 @@ private FeatureResolver(
this.parameters = parameters;
this.engineDescriptor = engineDescriptor;
this.packageFilter = packageFilter;
this.namingStrategy = new CucumberEngineOptions(parameters).namingStrategy();
CucumberEngineOptions options = new CucumberEngineOptions(parameters);
this.namingStrategy = options.namingStrategy();
CachingFeatureParser featureParser = createFeatureParser(options);
this.featureScanner = new ResourceScanner<>(
ClassLoaders::getDefaultClassLoader,
FeatureIdentifier::isFeature,
featureParser::parseResource);
}

private static CachingFeatureParser createFeatureParser(CucumberEngineOptions options) {
Supplier<ClassLoader> classLoader = FeatureResolver.class::getClassLoader;
UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader, options);
UuidGenerator uuidGenerator = uuidGeneratorServiceLoader.loadUuidGenerator();
FeatureParser featureParser = new FeatureParser(uuidGenerator::generateId);
return new CachingFeatureParser(featureParser);
}

static FeatureResolver create(
Expand Down
9 changes: 6 additions & 3 deletions cucumber-junit/src/main/java/io/cucumber/junit/Cucumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.cucumber.core.runtime.ThreadLocalObjectFactorySupplier;
import io.cucumber.core.runtime.ThreadLocalRunnerSupplier;
import io.cucumber.core.runtime.TimeServiceEventBus;
import io.cucumber.core.runtime.UuidGeneratorServiceLoader;
import org.apiguardian.api.API;
import org.junit.AfterClass;
import org.junit.BeforeClass;
Expand All @@ -38,7 +39,6 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.Supplier;

Expand Down Expand Up @@ -146,11 +146,14 @@ public Cucumber(Class<?> clazz) throws InitializationError {
.parse(CucumberProperties.fromSystemProperties())
.build(junitEnvironmentOptions);

this.bus = synchronize(new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID));
Supplier<ClassLoader> classLoader = ClassLoaders::getDefaultClassLoader;
UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader,
runtimeOptions);
this.bus = synchronize(
new TimeServiceEventBus(Clock.systemUTC(), uuidGeneratorServiceLoader.loadUuidGenerator()));

// Parse the features early. Don't proceed when there are lexer errors
FeatureParser parser = new FeatureParser(bus::generateId);
Supplier<ClassLoader> classLoader = ClassLoaders::getDefaultClassLoader;
FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions,
parser);
List<Feature> features = featureSupplier.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import io.cucumber.core.runtime.ThreadLocalObjectFactorySupplier;
import io.cucumber.core.runtime.ThreadLocalRunnerSupplier;
import io.cucumber.core.runtime.TimeServiceEventBus;
import io.cucumber.core.runtime.UuidGeneratorServiceLoader;
import org.apiguardian.api.API;

import java.time.Clock;
import java.util.List;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.Supplier;

Expand Down Expand Up @@ -98,9 +98,12 @@ public TestNGCucumberRunner(Class<?> clazz, CucumberPropertiesProvider propertie
.enablePublishPlugin()
.build(environmentOptions);

EventBus bus = synchronize(new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID));

Supplier<ClassLoader> classLoader = ClassLoaders::getDefaultClassLoader;
UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader,
runtimeOptions);
EventBus bus = synchronize(
new TimeServiceEventBus(Clock.systemUTC(), uuidGeneratorServiceLoader.loadUuidGenerator()));

FeatureParser parser = new FeatureParser(bus::generateId);
FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions,
parser);
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@
<roots>
<root>7.0.0</root>
<root>7.2.0</root>
<root>7.20.0</root>
<root>internal</root>
<root>testng</root>
<root>guice</root>
Expand Down

0 comments on commit 8029e93

Please sign in to comment.