From 39e815d1a44339d5ba87b7e4c9f6bf8d100d2382 Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Tue, 14 Mar 2023 08:02:21 +0100 Subject: [PATCH 1/8] feat: added UUID generator selection through SPI for https://github.com/cucumber/cucumber-jvm/issues/2698 --- .revapi/api-changes.json | 24 ++ CHANGELOG.md | 3 + compatibility/pom.xml | 2 +- cucumber-archetype/pom.xml | 2 +- cucumber-bom/pom.xml | 40 +-- cucumber-cdi2/pom.xml | 2 +- cucumber-core/README.md | 20 ++ cucumber-core/pom.xml | 2 +- .../cucumber/core/cli/CommandlineOptions.java | 2 + .../eventbus/IncrementingUuidGenerator.java | 40 +++ .../io/cucumber/core/eventbus/Options.java | 7 + .../core/eventbus/RandomUuidGenerator.java | 14 + .../cucumber/core/eventbus/UuidGenerator.java | 13 + .../options/CommandlineOptionsParser.java | 5 + .../io/cucumber/core/options/Constants.java | 9 + .../CucumberOptionsAnnotationParser.java | 10 + .../options/CucumberPropertiesParser.java | 6 + .../cucumber/core/options/RuntimeOptions.java | 14 +- .../core/options/RuntimeOptionsBuilder.java | 11 + .../core/options/UuidGeneratorParser.java | 27 ++ .../java/io/cucumber/core/runner/Options.java | 3 + .../io/cucumber/core/runtime/Runtime.java | 10 +- .../runtime/UuidGeneratorServiceLoader.java | 130 +++++++++ .../io.cucumber.core.eventbus.UuidGenerator | 2 + .../io/cucumber/core/options/USAGE.txt | 7 + .../IncrementingUuidGeneratorTest.java | 62 ++++ .../eventbus/RandomUuidGeneratorTest.java | 24 ++ .../options/CommandlineOptionsParserTest.java | 13 +- .../core/options/CucumberOptions.java | 2 + .../CucumberOptionsAnnotationParserTest.java | 18 ++ .../core/options/CucumberPropertiesTest.java | 9 +- .../core/options/NoUuidGenerator.java | 20 ++ .../options/RuntimeOptionsBuilderTest.java | 22 ++ .../core/options/UuidGeneratorParserTest.java | 53 ++++ .../ObjectFactoryServiceLoaderTest.java | 171 ++++++++--- .../runtime/ObjectFactoryServiceLoaderTest.md | 18 ++ .../runtime/ServiceLoaderTestClassLoader.java | 96 +++++++ .../UuidGeneratorServiceLoaderTest.java | 268 ++++++++++++++++++ .../runtime/UuidGeneratorServiceLoaderTest.md | 17 ++ cucumber-deltaspike/pom.xml | 2 +- cucumber-gherkin-messages/pom.xml | 2 +- cucumber-gherkin/pom.xml | 2 +- cucumber-guice/pom.xml | 2 +- cucumber-jakarta-cdi/pom.xml | 2 +- cucumber-jakarta-openejb/pom.xml | 2 +- cucumber-java/pom.xml | 2 +- cucumber-java8/pom.xml | 2 +- cucumber-junit-platform-engine/pom.xml | 2 +- .../junit/platform/engine/Constants.java | 10 + .../engine/CucumberEngineOptions.java | 13 +- .../engine/CucumberEngineOptionsTest.java | 21 +- cucumber-junit/pom.xml | 2 +- .../io/cucumber/junit/CucumberOptions.java | 11 + .../junit/JUnitCucumberOptionsProvider.java | 5 + .../io/cucumber/junit/NoUuidGenerator.java | 20 ++ .../JUnitCucumberOptionsProviderTest.java | 25 +- cucumber-kotlin-java8/pom.xml | 2 +- cucumber-openejb/pom.xml | 2 +- cucumber-picocontainer/pom.xml | 2 +- cucumber-plugin/pom.xml | 2 +- cucumber-spring/pom.xml | 2 +- cucumber-testng/pom.xml | 2 +- .../io/cucumber/testng/CucumberOptions.java | 11 + .../io/cucumber/testng/NoUuidGenerator.java | 20 ++ .../testng/TestNGCucumberOptionsProvider.java | 5 + .../TestNGCucumberOptionsProviderTest.java | 25 +- datatable-matchers/pom.xml | 2 +- datatable/pom.xml | 2 +- docstring/pom.xml | 2 +- examples/calculator-java-cli/pom.xml | 2 +- examples/calculator-java-junit4/pom.xml | 2 +- examples/calculator-java-junit5/pom.xml | 2 +- examples/calculator-java-testng/pom.xml | 2 +- examples/calculator-java8-cli/pom.xml | 2 +- examples/pom.xml | 2 +- examples/spring-java-junit5/pom.xml | 2 +- examples/wicket-java-junit4/pom.xml | 2 +- .../wicket-java-junit4/wicket-main/pom.xml | 2 +- .../wicket-java-junit4/wicket-test/pom.xml | 2 +- pom.xml | 4 +- 80 files changed, 1320 insertions(+), 106 deletions(-) create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java create mode 100644 cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator create mode 100644 cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md create mode 100644 cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java create mode 100644 cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java diff --git a/.revapi/api-changes.json b/.revapi/api-changes.json index 5bd2911d71..4355ce9dd1 100644 --- a/.revapi/api-changes.json +++ b/.revapi/api-changes.json @@ -181,6 +181,18 @@ "code": "java.method.finalMethodAddedToNonFinalClass", "new": "method java.lang.Long io.cucumber.core.internal.com.fasterxml.jackson.databind.deser.std.StdDeserializer::_parseLong(io.cucumber.core.internal.com.fasterxml.jackson.databind.DeserializationContext, java.lang.String) throws java.io.IOException", "justification": "Internal API" + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method java.lang.Class io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions::uuidGenerator()", + "justification": "Internal API" + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method java.lang.Class io.cucumber.core.runner.Options::getUuidGeneratorClass()", + "justification": "Internal API" } ] } @@ -331,6 +343,12 @@ "code": "java.method.defaultMethodAddedToInterface", "new": "method java.util.Set org.testng.ITestNGMethod::upstreamDependencies()", "justification": "Third party api change" + }, + { + "ignore": true, + "code": "java.class.externalClassExposedInAPI", + "new": "interface io.cucumber.core.eventbus.UuidGenerator", + "justification": "Part of cucumber API" } ] } @@ -383,6 +401,12 @@ "new": "method int org.junit.platform.engine.ConfigurationParameters::size()", "annotation": "@org.apiguardian.api.API(status = org.apiguardian.api.API.Status.DEPRECATED, since = \"1.9\")", "justification": "API consumed from JUnit 5" + }, + { + "ignore": true, + "code": "java.class.externalClassExposedInAPI", + "new": "interface io.cucumber.core.eventbus.UuidGenerator", + "justification": "Part of cucumber API" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac08ee839..0aeb6dbd72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [Core] Improved event bus performance using UUID generator selectable through SPI ([#2703](https://github.com/cucumber/cucumber-jvm/pull/2703) Julien Kronegg) + ## [7.11.1] - 2023-01-27 ### Added - [Core] Warn when `cucumber.options` is used ([#2685](https://github.com/cucumber/cucumber-jvm/pull/2685) M.P. Korstanje) diff --git a/compatibility/pom.xml b/compatibility/pom.xml index 9d62f76276..48fd6e00f0 100644 --- a/compatibility/pom.xml +++ b/compatibility/pom.xml @@ -4,7 +4,7 @@ cucumber-jvm io.cucumber - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT 4.0.0 diff --git a/cucumber-archetype/pom.xml b/cucumber-archetype/pom.xml index ea65416237..28f7cea336 100644 --- a/cucumber-archetype/pom.xml +++ b/cucumber-archetype/pom.xml @@ -6,7 +6,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-archetype diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index 78654cf628..f19b258c37 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -3,7 +3,7 @@ cucumber-jvm io.cucumber - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT 4.0.0 pom @@ -63,97 +63,97 @@ io.cucumber cucumber-cdi2 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-core - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber datatable - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber datatable-matchers - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-deltaspike - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber docstring - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-gherkin - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-gherkin-messages - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-guice - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-jakarta-cdi - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-java - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-java8 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-junit - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-junit-platform-engine - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-openejb - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-picocontainer - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-plugin - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-spring - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-testng - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT diff --git a/cucumber-cdi2/pom.xml b/cucumber-cdi2/pom.xml index 24debd11b4..257cb96af0 100644 --- a/cucumber-cdi2/pom.xml +++ b/cucumber-cdi2/pom.xml @@ -14,7 +14,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-cdi2 diff --git a/cucumber-core/README.md b/cucumber-core/README.md index cdd5a7824f..6e07801ada 100644 --- a/cucumber-core/README.md +++ b/cucumber-core/README.md @@ -52,6 +52,9 @@ cucumber.plugin= # comma separated plugin strings. cucumber.object-factory= # object factory class name. # example: com.example.MyObjectFactory +cucumber.uuid-generator= # UUID generator class name. + # example: com.example.MyUuidGenerator + cucumber.publish.enabled # true or false. default: false # enable publishing of test results @@ -79,6 +82,23 @@ They are respectively responsible for discovering glue classes, registering step definitions, and creating instances of said glue classes. Backend and object factory implementations are discovered via SPI. +## Event bus ## + +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: + +| 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 | + +The performance gain on real project depend on the feature size. + +When not specified, the `RandomUuidGenerator` is used. + ## Plugin ## By implementing the Plugin interface classes can listen to execution events diff --git a/cucumber-core/pom.xml b/cucumber-core/pom.xml index bc7aaac6f8..d246466ee0 100644 --- a/cucumber-core/pom.xml +++ b/cucumber-core/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-core diff --git a/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java b/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java index 229697b122..3d9ea17d7d 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java @@ -66,6 +66,8 @@ public final class CommandlineOptions { public static final String OBJECT_FACTORY = "--object-factory"; + public static final String UUID_GENERATOR = "--uuid-generator"; + private CommandlineOptions() { } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java new file mode 100644 index 0000000000..35fa782e79 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java @@ -0,0 +1,40 @@ +package io.cucumber.core.eventbus; + +import io.cucumber.core.exception.CucumberException; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 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. If you use Cucumber in + * multi-JVM setup, you should use #RandomUuidGenerator instead. Note that the + * UUID version and variant is not guaranteed to be stable. + */ +public class IncrementingUuidGenerator implements UuidGenerator { + private static final AtomicLong sessionCounter = new AtomicLong(Long.MIN_VALUE); + + private final long sessionId; + private final AtomicLong counter = new AtomicLong(Long.MIN_VALUE); + + public IncrementingUuidGenerator() { + sessionId = sessionCounter.incrementAndGet(); + } + + /** + * Generate a new UUID. Will throw an exception when out of capacity. + * + * @return a non-null UUID + * @throws CucumberException when out of capacity + */ + @Override + public UUID get() { + long leastSigBits = counter.incrementAndGet(); + if (leastSigBits == Long.MAX_VALUE) { + throw new CucumberException( + "Out of IncrementingUuidGenerator capacity. Please use the RandomUuidGenerator instead."); + } + return new UUID(sessionId, leastSigBits); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java new file mode 100644 index 0000000000..b14ef7a05e --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java @@ -0,0 +1,7 @@ +package io.cucumber.core.eventbus; + +public interface Options { + + Class getUuidGeneratorClass(); + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java new file mode 100644 index 0000000000..6ecdf84ac2 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java @@ -0,0 +1,14 @@ +package io.cucumber.core.eventbus; + +import java.util.UUID; + +/** + * UUID generator based on random numbers. The generator is thread-safe and + * supports multi-jvm usage of Cucumber. + */ +public class RandomUuidGenerator implements UuidGenerator { + @Override + public UUID get() { + return UUID.randomUUID(); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java new file mode 100644 index 0000000000..6557576ee4 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java @@ -0,0 +1,13 @@ +package io.cucumber.core.eventbus; + +import org.apiguardian.api.API; + +import java.util.UUID; +import java.util.function.Supplier; + +/** + * SPI (Service Provider Interface) to generate UUIDs. + */ +@API(status = API.Status.STABLE) +public interface UuidGenerator extends Supplier { +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java index ffa3502a1f..88db18e236 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java @@ -53,12 +53,14 @@ import static io.cucumber.core.cli.CommandlineOptions.TAGS; import static io.cucumber.core.cli.CommandlineOptions.TAGS_SHORT; import static io.cucumber.core.cli.CommandlineOptions.THREADS; +import static io.cucumber.core.cli.CommandlineOptions.UUID_GENERATOR; import static io.cucumber.core.cli.CommandlineOptions.VERSION; import static io.cucumber.core.cli.CommandlineOptions.VERSION_SHORT; import static io.cucumber.core.cli.CommandlineOptions.WIP; import static io.cucumber.core.cli.CommandlineOptions.WIP_SHORT; import static io.cucumber.core.options.ObjectFactoryParser.parseObjectFactory; import static io.cucumber.core.options.OptionsFileParser.parseFeatureWithLinesFile; +import static io.cucumber.core.options.UuidGeneratorParser.parseUuidGenerator; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; @@ -167,6 +169,9 @@ private RuntimeOptionsBuilder parse(List args) { } else if (arg.equals(OBJECT_FACTORY)) { String objectFactoryClassName = removeArgFor(arg, args); parsedOptions.setObjectFactoryClass(parseObjectFactory(objectFactoryClassName)); + } else if (arg.equals(UUID_GENERATOR)) { + String uuidGeneratorClassName = removeArgFor(arg, args); + parsedOptions.setUuidGeneratorClass(parseUuidGenerator(uuidGeneratorClassName)); } else if (arg.startsWith("-")) { out.println("Unknown option: " + arg); printUsage(); diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java index dc88f3c281..8681e83d91 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.runtime.ObjectFactoryServiceLoader; +import io.cucumber.core.runtime.UuidGeneratorServiceLoader; public final class Constants { @@ -118,6 +119,14 @@ public final class Constants { */ public static final String OBJECT_FACTORY_PROPERTY_NAME = "cucumber.object-factory"; + /** + * Property name used to select a specific UUID generator implementation: + * {@value} + * + * @see UuidGeneratorServiceLoader + */ + public static final String UUID_GENERATOR_PROPERTY_NAME = "cucumber.uuid-generator"; + /** * Property name formerly used to pass command line options to Cucumber: * {@value} This property is no longer read by Cucumber. Please use any of diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java index 684af34c2d..576ac6ff07 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.feature.GluePath; @@ -45,6 +46,7 @@ public RuntimeOptionsBuilder parse(Class clazz) { addGlue(options, args); addFeatures(options, args); addObjectFactory(options, args); + addUuidGenerator(options, args); } } @@ -149,6 +151,12 @@ private void addObjectFactory(CucumberOptions options, RuntimeOptionsBuilder arg } } + private void addUuidGenerator(CucumberOptions options, RuntimeOptionsBuilder args) { + if (options.uuidGenerator() != null) { + args.setUuidGeneratorClass(options.uuidGenerator()); + } + } + private void addDefaultFeaturePathIfNoFeaturePathIsSpecified(RuntimeOptionsBuilder args, Class clazz) { if (!featuresSpecified) { String packageName = packagePath(clazz); @@ -208,6 +216,8 @@ public interface CucumberOptions { Class objectFactory(); + Class uuidGenerator(); + } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java index db2aab247f..d4bb7d2aaf 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java @@ -32,6 +32,7 @@ import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME; import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME; import static io.cucumber.core.options.Constants.SNIPPET_TYPE_PROPERTY_NAME; +import static io.cucumber.core.options.Constants.UUID_GENERATOR_PROPERTY_NAME; import static io.cucumber.core.options.Constants.WIP_PROPERTY_NAME; import static io.cucumber.core.options.OptionsFileParser.parseFeatureWithLinesFile; import static java.util.Arrays.stream; @@ -102,6 +103,11 @@ public RuntimeOptionsBuilder parse(CucumberPropertiesProvider properties) { ObjectFactoryParser::parseObjectFactory, builder::setObjectFactoryClass); + parse(properties, + UUID_GENERATOR_PROPERTY_NAME, + UuidGeneratorParser::parseUuidGenerator, + builder::setUuidGeneratorClass); + parse(properties, OPTIONS_PROPERTY_NAME, identity(), diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java index 0c0490db5a..4db93daac4 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.order.PickleOrder; import io.cucumber.core.order.StandardPickleOrders; @@ -33,7 +34,8 @@ public final class RuntimeOptions implements io.cucumber.core.runner.Options, io.cucumber.core.plugin.Options, io.cucumber.core.filter.Options, - io.cucumber.core.backend.Options { + io.cucumber.core.backend.Options, + io.cucumber.core.eventbus.Options { private final List glue = new ArrayList<>(); private final List tagExpressions = new ArrayList<>(); @@ -48,6 +50,7 @@ public final class RuntimeOptions implements private PickleOrder pickleOrder = StandardPickleOrders.lexicalUriOrder(); private int count = 0; private Class objectFactoryClass; + private Class uuidGeneratorClass; private String publishToken; private boolean publish; private boolean publishQuiet; @@ -158,6 +161,15 @@ void setObjectFactoryClass(Class objectFactoryClass) { this.objectFactoryClass = objectFactoryClass; } + @Override + public Class getUuidGeneratorClass() { + return uuidGeneratorClass; + } + + void setUuidGeneratorClass(Class uuidGeneratorClass) { + this.uuidGeneratorClass = uuidGeneratorClass; + } + void setSnippetType(SnippetType snippetType) { this.snippetType = snippetType; } diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java index db7bddafa5..58b6792bc5 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.order.PickleOrder; @@ -31,6 +32,7 @@ public final class RuntimeOptionsBuilder { private PickleOrder parsedPickleOrder = null; private Integer parsedCount = null; private Class parsedObjectFactoryClass = null; + private Class parsedUuidGeneratorClass = null; private Boolean addDefaultSummaryPrinter = null; private boolean addDefaultGlueIfAbsent; private boolean addDefaultFeaturePathIfAbsent; @@ -132,6 +134,10 @@ public RuntimeOptions build(RuntimeOptions runtimeOptions) { runtimeOptions.setObjectFactoryClass(parsedObjectFactoryClass); } + if (parsedUuidGeneratorClass != null) { + runtimeOptions.setUuidGeneratorClass(parsedUuidGeneratorClass); + } + if (addDefaultSummaryPrinter != null && addDefaultSummaryPrinter) { runtimeOptions.addDefaultSummaryPrinter(); } @@ -239,6 +245,11 @@ public RuntimeOptionsBuilder setObjectFactoryClass(Class uuidGeneratorClass) { + this.parsedUuidGeneratorClass = uuidGeneratorClass; + return this; + } + public RuntimeOptionsBuilder setPublishToken(String token) { this.parsedPublishToken = token; return this; diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java new file mode 100644 index 0000000000..ecc633f211 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java @@ -0,0 +1,27 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.UuidGenerator; + +public final class UuidGeneratorParser { + + private UuidGeneratorParser() { + + } + + @SuppressWarnings("unchecked") + public static Class parseUuidGenerator(String cucumberUuidGenerator) { + Class uuidGeneratorClass; + try { + uuidGeneratorClass = Class.forName(cucumberUuidGenerator); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + String.format("Could not load UUID generator class for '%s'", cucumberUuidGenerator), e); + } + if (!UuidGenerator.class.isAssignableFrom(uuidGeneratorClass)) { + throw new IllegalArgumentException(String.format("UUID generator class '%s' was not a subclass of '%s'", + uuidGeneratorClass, UuidGenerator.class)); + } + return (Class) uuidGeneratorClass; + } + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java index a1094523c5..0fe0ffb807 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java @@ -1,6 +1,7 @@ package io.cucumber.core.runner; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.snippets.SnippetType; import java.net.URI; @@ -16,4 +17,6 @@ public interface Options { Class getObjectFactoryClass(); + Class getUuidGeneratorClass(); + } diff --git a/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java b/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java index 1a7b92c23f..8fba565bcb 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.UUID; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -138,7 +137,7 @@ public byte exitStatus() { public static class Builder { - private EventBus eventBus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + private EventBus eventBus; private Supplier classLoader = ClassLoaders::getDefaultClassLoader; private RuntimeOptions runtimeOptions = RuntimeOptions.defaultOptions(); private BackendSupplier backendSupplier; @@ -197,6 +196,13 @@ public Runtime build() { final ExitStatus exitStatus = new ExitStatus(runtimeOptions); plugins.addPlugin(exitStatus); + if (this.eventBus == null) { + final UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader( + classLoader, + runtimeOptions); + this.eventBus = new TimeServiceEventBus(Clock.systemUTC(), + uuidGeneratorServiceLoader.loadUuidGenerator()); + } final EventBus eventBus = synchronize(this.eventBus); if (runtimeOptions.isMultiThreaded()) { diff --git a/cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java b/cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java new file mode 100644 index 0000000000..d8fbb2baf2 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java @@ -0,0 +1,130 @@ +package io.cucumber.core.runtime; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.Options; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; +import io.cucumber.core.exception.CucumberException; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; + +/** + * Loads an instance of {@link UuidGenerator} using the {@link ServiceLoader} + * mechanism. + *

+ * Will load an instance of the class provided by + * {@link Options#getUuidGeneratorClass()}. If + * {@link Options#getUuidGeneratorClass()} does not provide a class, if there is + * exactly one {@code UuidGenerator} instance available that instance will be + * used. + *

+ * Otherwise {@link RandomUuidGenerator} with no dependency injection + */ +public final class UuidGeneratorServiceLoader { + + private final Supplier classLoaderSupplier; + private final Options options; + + public UuidGeneratorServiceLoader(Supplier classLoaderSupplier, Options options) { + this.classLoaderSupplier = requireNonNull(classLoaderSupplier); + this.options = requireNonNull(options); + } + + UuidGenerator loadUuidGenerator() { + Class objectFactoryClass = options.getUuidGeneratorClass(); + ClassLoader classLoader = classLoaderSupplier.get(); + ServiceLoader loader = ServiceLoader.load(UuidGenerator.class, classLoader); + if (objectFactoryClass == null) { + return loadSingleUuidGeneratorOrDefault(loader); + } + + return loadSelectedUuidGenerator(loader, objectFactoryClass); + } + + private static UuidGenerator loadSingleUuidGeneratorOrDefault(ServiceLoader loader) { + Iterator uuidGenerators = loader.iterator(); + + // categorize the UUID generators (random, incrementing or external) + UuidGenerator randomGenerator = null; + UuidGenerator incrementingGenerator = null; + UuidGenerator externalGenerator = null; + while (uuidGenerators.hasNext()) { + UuidGenerator uuidGenerator = uuidGenerators.next(); + if (uuidGenerator instanceof RandomUuidGenerator) { + randomGenerator = uuidGenerator; + } else if (uuidGenerator instanceof IncrementingUuidGenerator) { + incrementingGenerator = uuidGenerator; + } else { + if (externalGenerator != null) { + // we have multiple external generators, which is an error + throw new CucumberException(getMultipleUuidGeneratorLogMessage( + Arrays.asList(externalGenerator, uuidGenerator))); + } + externalGenerator = uuidGenerator; + } + } + + // decide which generator to use + if (externalGenerator != null) { + // we have a single external generator + return externalGenerator; + } else if (randomGenerator != null) { + // we don't have any external generators, use random if available + return randomGenerator; + } else if (incrementingGenerator != null) { + // we don't have any external generators and no random, use + // incrementing if available + return incrementingGenerator; + } else { + // we don't have any generators at all, throw an error + throw new CucumberException("" + + "Could not find any UUID generator.\n" + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "This typically happens when using shaded jars. Make sure\n" + + "to merge all SPI definitions in META-INF/services correctly"); + } + } + + private static UuidGenerator loadSelectedUuidGenerator( + ServiceLoader loader, + Class uuidGeneratorClass + ) { + for (UuidGenerator uuidGenerator : loader) { + if (uuidGeneratorClass.equals(uuidGenerator.getClass())) { + return uuidGenerator; + } + } + + throw new CucumberException("" + + "Could not find UUID generator " + uuidGeneratorClass.getName() + ".\n" + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "Has the class been registered with SPI and is it available on\n" + + "the classpath?"); + } + + private static String getMultipleUuidGeneratorLogMessage(List uuidGenerators) { + String factoryNames = Stream.of(uuidGenerators) + .map(Object::getClass) + .map(Class::getName) + .collect(Collectors.joining(", ")); + + return "More than one Cucumber UuidGenerator was found on the classpath\n" + + "\n" + + "Found: " + factoryNames + "\n" + + "\n" + + "You can either remove the unnecessary SPI dependencies from your classpath\n" + + "or use the `cucumber.uuid-generator` property\n" + + "or `@CucumberOptions(uuidGenerator=...)` to select one UUID generator.\n"; + } + +} diff --git a/cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator b/cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator new file mode 100644 index 0000000000..c7c37e3f7b --- /dev/null +++ b/cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator @@ -0,0 +1,2 @@ +io.cucumber.core.eventbus.RandomUuidGenerator +io.cucumber.core.eventbus.IncrementingUuidGenerator diff --git a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt index 58a57584b1..17a05569ba 100644 --- a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt +++ b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt @@ -73,6 +73,13 @@ Options: be specified in: META-INF/services/io.cucumber.core.backend.ObjectFactory + --uuid-generator CLASSNAME Uses the class specified by CLASSNAME + as UUID generator. Be aware that the + class is loaded through a service + loader and therefore also needs to + be specified in: + META-INF/services/io.cucumber.core.eventbus.UuidGenerator + Feature path examples: When no feature path is provided cucumber will scan the classpath root diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java new file mode 100644 index 0000000000..a3fab341f1 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java @@ -0,0 +1,62 @@ +package io.cucumber.core.eventbus; + +import io.cucumber.core.exception.CucumberException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class IncrementingUuidGeneratorTest { + @Test + void generates_different_non_null_uuids() { + // Given + UuidGenerator generator = new IncrementingUuidGenerator(); + UUID uuid1 = generator.get(); + + // When + UUID uuid2 = generator.get(); + + // Then + assertNotNull(uuid1); + assertNotNull(uuid2); + assertNotEquals(uuid1, uuid2); + } + + @Test + void raises_exception_when_out_of_range() throws NoSuchFieldException, IllegalAccessException { + // Given + UuidGenerator generator = new IncrementingUuidGenerator(); + Field counterField = IncrementingUuidGenerator.class.getDeclaredField("counter"); + counterField.setAccessible(true); + AtomicLong counter = (AtomicLong) counterField.get(generator); + counter.set(Long.MAX_VALUE - 1); + + // When + CucumberException cucumberException = assertThrows(CucumberException.class, generator::get); + + // Then + assertThat(cucumberException.getMessage(), + Matchers.containsString("Out of IncrementingUuidGenerator capacity")); + } + + @Test + void same_thread_generates_different_UuidGenerators() { + // Given + UuidGenerator generator1 = new IncrementingUuidGenerator(); + UuidGenerator generator2 = new IncrementingUuidGenerator(); + + // When + UUID uuid1 = generator1.get(); + UUID uuid2 = generator2.get(); + + // Then + assertNotNull(uuid1); + assertNotNull(uuid2); + assertNotEquals(uuid1, uuid2); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java new file mode 100644 index 0000000000..c578c04d1e --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java @@ -0,0 +1,24 @@ +package io.cucumber.core.eventbus; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class RandomUuidGeneratorTest { + @Test + void generates_different_non_null_uuids() { + // Given + UuidGenerator generator = new RandomUuidGenerator(); + UUID uuid1 = generator.get(); + + // When + UUID uuid2 = generator.get(); + + // Then + assertNotNull(uuid1); + assertNotNull(uuid2); + assertNotEquals(uuid1, uuid2); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java index 74824c90aa..5d31229ae3 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import io.cucumber.core.feature.TestFeatureParser; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.gherkin.Pickle; @@ -76,6 +77,16 @@ void testParseWithObjectFactoryArgument() { assertThat(options.getObjectFactoryClass(), Is.is(equalTo(TestObjectFactory.class))); } + @Test + void testParseWithUuidGeneratorArgument() { + RuntimeOptionsBuilder optionsBuilder = parser.parse("--uuid-generator", + IncrementingUuidGenerator.class.getName()); + assertNotNull(optionsBuilder); + RuntimeOptions options = optionsBuilder.build(); + assertNotNull(options); + assertThat(options.getUuidGeneratorClass(), Is.is(equalTo(IncrementingUuidGenerator.class))); + } + @Test void has_version_from_properties_file() { parser.parse("--version"); @@ -84,7 +95,7 @@ void has_version_from_properties_file() { } private String output() { - return new String(out.toByteArray(), StandardCharsets.UTF_8); + return out.toString(StandardCharsets.UTF_8); } @Test diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java index 267ac37c55..cf4457c7f2 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java @@ -33,6 +33,8 @@ Class objectFactory() default NoObjectFactory.class; + Class uuidGenerator() default NoUuidGenerator.class; + String[] junit() default {}; } diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java index 43fb536e0b..e078ce47e2 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java @@ -1,6 +1,8 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; import io.cucumber.core.plugin.HtmlFormatter; import io.cucumber.core.plugin.NoPublishFormatter; @@ -252,6 +254,13 @@ void cannot_create_with_glue_and_extra_glue() { is(equalTo("glue and extraGlue cannot be specified at the same time"))); } + @Test + void uuid_generator() { + RuntimeOptions runtimeOptions = parser().parse(ClassWithUuidGenerator.class).build(); + + assertThat(runtimeOptions.getUuidGeneratorClass(), is(IncrementingUuidGenerator.class)); + } + @CucumberOptions(snippets = SnippetType.CAMELCASE) private static class Snippets { // empty @@ -363,6 +372,11 @@ private static class ClassWithGlueAndExtraGlue { // empty } + @CucumberOptions(uuidGenerator = IncrementingUuidGenerator.class) + private static class ClassWithUuidGenerator extends ClassWithGlue { + // empty + } + private static class CoreCucumberOptions implements CucumberOptionsAnnotationParser.CucumberOptions { private final CucumberOptions annotation; @@ -426,6 +440,10 @@ public Class objectFactory() { return (annotation.objectFactory() == NoObjectFactory.class) ? null : annotation.objectFactory(); } + @Override + public Class uuidGenerator() { + return (annotation.uuidGenerator() == NoUuidGenerator.class) ? null : annotation.uuidGenerator(); + } } private static class CoreCucumberOptionsProvider implements CucumberOptionsAnnotationParser.OptionsProvider { diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java index a4f6087b76..85c5ee9385 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java @@ -16,7 +16,14 @@ class CucumberPropertiesTest { @Test void looks_up_value_from_environment() { - assertThat(CucumberProperties.fromEnvironment().get("PATH"), is(notNullValue())); + Map properties = CucumberProperties.fromEnvironment(); + String path = properties.get("PATH"); + if (path == null) { + // on some Windows flavors, the PATH environment variable is named + // "Path" + path = properties.get("Path"); + } + assertThat(path, is(notNullValue())); } @Test diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java b/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java new file mode 100644 index 0000000000..a8ddb653b0 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java @@ -0,0 +1,20 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.UuidGenerator; + +import java.util.UUID; + +/** + * This UUID generator does nothing. It is solely needed for marking purposes. + */ +final class NoUuidGenerator implements UuidGenerator { + + private NoUuidGenerator() { + // No need for instantiation + } + + @Override + public UUID get() { + return null; + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java new file mode 100644 index 0000000000..02bb028bbf --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java @@ -0,0 +1,22 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RuntimeOptionsBuilderTest { + + @Test + void build() { + // Given + RuntimeOptionsBuilder builder = new RuntimeOptionsBuilder() + .setUuidGeneratorClass(IncrementingUuidGenerator.class); + + // When + RuntimeOptions runtimeOptions = builder.build(); + + // Then + assertEquals(IncrementingUuidGenerator.class, runtimeOptions.getUuidGeneratorClass()); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java new file mode 100644 index 0000000000..012a2ec87c --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java @@ -0,0 +1,53 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class UuidGeneratorParserTest { + + @Test + void parseUuidGenerator_IncrementingUuidGenerator() { + // When + Class uuidGeneratorClass = UuidGeneratorParser + .parseUuidGenerator(IncrementingUuidGenerator.class.getName()); + + // Then + assertEquals(IncrementingUuidGenerator.class, uuidGeneratorClass); + } + + @Test + void parseUuidGenerator_RandomUuidGenerator() { + // When + Class uuidGeneratorClass = UuidGeneratorParser + .parseUuidGenerator(RandomUuidGenerator.class.getName()); + + // Then + assertEquals(RandomUuidGenerator.class, uuidGeneratorClass); + } + + @Test + void parseUuidGenerator_not_a_generator() { + // When + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> UuidGeneratorParser.parseUuidGenerator(String.class.getName())); + + // Then + assertThat(exception.getMessage(), Matchers.containsString("not a subclass")); + } + + @Test + void parseUuidGenerator_not_a_class() { + // When + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> UuidGeneratorParser.parseUuidGenerator("java.lang.NonExistingClassName")); + + // Then + assertThat(exception.getMessage(), Matchers.containsString("Could not load UUID generator class")); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java index f6f5920a80..14580387cd 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java @@ -3,25 +3,50 @@ import io.cucumber.core.backend.DefaultObjectFactory; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.backend.Options; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; import java.util.function.Supplier; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; +/** + * @see test-cases description + */ class ObjectFactoryServiceLoaderTest { + /** + * Test case #1 + */ + @Test + void shouldThrowIfDefaultObjectFactoryServiceCouldNotBeLoaded() { + Options options = () -> null; + Supplier classLoader = () -> new ServiceLoaderTestClassLoader(ObjectFactory.class); + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + classLoader, + options); + + CucumberException exception = assertThrows(CucumberException.class, loader::loadObjectFactory); + assertThat(exception.getMessage(), is("" + + "Could not find any object factory.\n" + + "\n" + + "Cucumber uses SPI to discover object factory implementations.\n" + + "This typically happens when using shaded jars. Make sure\n" + + "to merge all SPI definitions in META-INF/services correctly")); + } + + /** + * Test case #2 + */ @Test void shouldLoadDefaultObjectFactoryService() { Options options = () -> null; @@ -31,6 +56,9 @@ void shouldLoadDefaultObjectFactoryService() { assertThat(loader.loadObjectFactory(), instanceOf(DefaultObjectFactory.class)); } + /** + * Test case #3 + */ @Test void shouldLoadSelectedObjectFactoryService() { Options options = () -> DefaultObjectFactory.class; @@ -40,37 +68,99 @@ void shouldLoadSelectedObjectFactoryService() { assertThat(loader.loadObjectFactory(), instanceOf(DefaultObjectFactory.class)); } + /** + * Test-case #4 + */ @Test - void shouldThrowIfDefaultObjectFactoryServiceCouldNotBeLoaded() { + void test_case_4() { + io.cucumber.core.backend.Options options = () -> null; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); + } + + /** + * Test-case #4 bis (reverse order) + */ + @Test + void test_case_4_with_services_in_reverse_order() { + io.cucumber.core.backend.Options options = () -> null; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + OtherFactory.class, + DefaultObjectFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); + } + + /** + * Test-case #5 + */ + @Test + void test_case_5() { + io.cucumber.core.backend.Options options = () -> DefaultObjectFactory.class; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(DefaultObjectFactory.class)); + } + + /** + * Test case #6 + */ + @Test + void test_case_6() { + // Given Options options = () -> null; - Supplier classLoader = () -> new FilteredClassLoader( - "META-INF/services/io.cucumber.core.backend.ObjectFactory"); ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( - classLoader, + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class, + YetAnotherFactory.class), options); + // When CucumberException exception = assertThrows(CucumberException.class, loader::loadObjectFactory); - assertThat(exception.getMessage(), is("" + - "Could not find any object factory.\n" + - "\n" + - "Cucumber uses SPI to discover object factory implementations.\n" + - "This typically happens when using shaded jars. Make sure\n" + - "to merge all SPI definitions in META-INF/services correctly")); + + // Then + assertThat(exception.getMessage(), + containsString("More than one Cucumber ObjectFactory was found on the classpath")); + } + + /** + * Test-case #7 + */ + @Test + void test_case_7() { + io.cucumber.core.backend.Options options = () -> OtherFactory.class; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class, + YetAnotherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); } + /** + * Test case #8 + */ @Test void shouldThrowIfSelectedObjectFactoryServiceCouldNotBeLoaded() { - Options options = () -> NoSuchObjectFactory.class; - Supplier classLoader = () -> new FilteredClassLoader( - "META-INF/services/io.cucumber.core.backend.ObjectFactory"); + Options options = () -> OtherFactory.class; ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( - classLoader, + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class), options); CucumberException exception = assertThrows(CucumberException.class, loader::loadObjectFactory); assertThat(exception.getMessage(), is("" + - "Could not find object factory io.cucumber.core.runtime.ObjectFactoryServiceLoaderTest$NoSuchObjectFactory.\n" + "Could not find object factory io.cucumber.core.runtime.ObjectFactoryServiceLoaderTest$OtherFactory.\n" + "\n" + "Cucumber uses SPI to discover object factory implementations.\n" + @@ -78,7 +168,20 @@ void shouldThrowIfSelectedObjectFactoryServiceCouldNotBeLoaded() { "the classpath?")); } - static class NoSuchObjectFactory implements ObjectFactory { + /** + * Test-case #9 + */ + @Test + void test_case_9() { + io.cucumber.core.backend.Options options = () -> null; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + OtherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); + } + + public static class FakeObjectFactory implements ObjectFactory { @Override public boolean addClass(Class glueClass) { @@ -102,25 +205,9 @@ public void stop() { } - private static class FilteredClassLoader extends URLClassLoader { - - private final Collection filteredResources; - - public FilteredClassLoader(String... filteredResources) { - super(new URL[0], FilteredClassLoader.class.getClassLoader()); - this.filteredResources = Arrays.asList(filteredResources); - } - - @Override - public Enumeration getResources(String name) throws IOException { - for (String filteredResource : filteredResources) { - if (name.equals(filteredResource)) { - return Collections.emptyEnumeration(); - } - } - return super.getResources(name); - } - + public static class OtherFactory extends FakeObjectFactory { } + public static class YetAnotherFactory extends FakeObjectFactory { + } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md new file mode 100644 index 0000000000..e5155b8b28 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md @@ -0,0 +1,18 @@ +# Testcases for `ObjectFactoryServiceLoader` + +| # | object-factory property | Available services | Result | +|---|-------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------| +| 1 | undefined | none | exception, no generators available | +| 2 | undefined | DefaultObjectFactory | DefaultObjectFactory used | +| 3 | DefaultObjectFactory | DefaultObjectFactory | DefaultObjectFactory used | +| 4 | undefined | DefaultObjectFactory, OtherFactory | OtherFactory used | +| 5 | DefaultObjectFactory | DefaultObjectFactory, OtherFactory | DefaultObjectFactory used | +| 6 | undefined | DefaultObjectFactory, OtherFactory, YetAnotherFactory | exception, cucumber couldn't decide multiple (non default) generators available | +| 7 | OtherFactory | DefaultObjectFactory, OtherFactory, YetAnotherFactory | OtherFactory used | +| 8 | OtherFactory | DefaultObjectFactory | exception, class not found through SPI | +| 9 | undefined | OtherFactory | OtherFactory used | + +Essentially this means that +* (2) Cucumber works by default +* (4) When adding a custom implementation to the class path it is used automatically +* When cucumber should not guess (5) or can not guess (7), the property is used to force a choice diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java new file mode 100644 index 0000000000..bfedc5a0d6 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java @@ -0,0 +1,96 @@ +package io.cucumber.core.runtime; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Collections; +import java.util.Enumeration; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Testing classloader for ServiceLoader. This classloader overrides the + * META-INF/services/ file with a custom definition. + */ +public class ServiceLoaderTestClassLoader extends URLClassLoader { + Class metaInfInterface; + Class[] implementingClasses; + + /** + * Constructs a classloader which has no + * META-INF/services/. + * + * @param metaInfInterface ServiceLoader interface + */ + public ServiceLoaderTestClassLoader(Class metaInfInterface) { + this(metaInfInterface, (Class[]) null); + } + + /** + * Constructs a fake META-INF/services/ file which + * contains the provided array of classes. When the implementingClasses + * array is null, the META-INF file will not be constructed. The classes + * from implementingClasses are not required to implement the + * metaInfInterface. + * + * @param metaInfInterface ServiceLoader interface + * @param implementingClasses potential subclasses of the ServiceLoader + * metaInfInterface + */ + public ServiceLoaderTestClassLoader(Class metaInfInterface, Class... implementingClasses) { + super(new URL[0], metaInfInterface.getClassLoader()); + if (!metaInfInterface.isInterface()) { + throw new IllegalArgumentException("the META-INF service " + metaInfInterface + " should be an interface"); + } + this.metaInfInterface = metaInfInterface; + this.implementingClasses = implementingClasses; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (name.equals("META-INF/services/" + metaInfInterface.getName())) { + if (implementingClasses == null) { + return Collections.emptyEnumeration(); + } + URL url = new URL("foo", "bar", 99, "/foobar", new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override + public void connect() { + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(Stream.of(implementingClasses) + .map(Class::getName) + .collect(Collectors.joining("\n")) + .getBytes()); + } + }; + } + }); + + return new Enumeration() { + boolean hasNext = true; + + @Override + public boolean hasMoreElements() { + return hasNext; + } + + @Override + public URL nextElement() { + hasNext = false; + return url; + } + }; + } + return super.getResources(name); + } + +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java new file mode 100644 index 0000000000..5843bbeedc --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java @@ -0,0 +1,268 @@ +package io.cucumber.core.runtime; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.Options; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; +import io.cucumber.core.exception.CucumberException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.jupiter.api.Assertions.*; + +/** + * @see test-cases description + */ +class UuidGeneratorServiceLoaderTest { + + /** + * | 1 | undefined | none | exception, no generators available | + */ + @Test + void test_case_1() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class), + options); + + CucumberException exception = assertThrows(CucumberException.class, loader::loadUuidGenerator); + assertThat(exception.getMessage(), is("" + + "Could not find any UUID generator.\n" + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "This typically happens when using shaded jars. Make sure\n" + + "to merge all SPI definitions in META-INF/services correctly")); + } + + /** + * | 2 | undefined | RandomUuidGenerator, IncrementingUuidGenerator | + * RandomUuidGenerator used | + */ + @Test + void test_case_2() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + UuidGeneratorServiceLoaderTest.class::getClassLoader, + options); + assertThat(loader.loadUuidGenerator(), instanceOf(RandomUuidGenerator.class)); + } + + /** + * | 3 | RandomUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator | RandomUuidGenerator used | + */ + @Test + void test_case_3() { + Options options = () -> RandomUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + UuidGeneratorServiceLoaderTest.class::getClassLoader, + options); + assertThat(loader.loadUuidGenerator(), instanceOf(RandomUuidGenerator.class)); + } + + /** + * | 4 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, + * OtherGenerator | OtherGenerator used | + */ + @Test + void test_case_4() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 4bis | undefined | OtherGenerator, RandomUuidGenerator, + * IncrementingUuidGenerator | OtherGenerator used | + */ + @Test + void test_case_4_bis() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + OtherGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 5 | RandomUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator, OtherGenerator | RandomUuidGenerator used | + */ + @Test + void test_case_5() { + Options options = () -> RandomUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(RandomUuidGenerator.class)); + } + + /** + * | 6 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, + * OtherGenerator, YetAnotherGenerator | exception, cucumber couldn't decide + * multiple (non default) generators available | + */ + @Test + void test_case_6() { + // Given + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class, + YetAnotherGenerator.class), + options); + + // When + CucumberException cucumberException = assertThrows(CucumberException.class, loader::loadUuidGenerator); + + // Then + assertThat(cucumberException.getMessage(), + Matchers.containsString("More than one Cucumber UuidGenerator was found on the classpath")); + } + + /** + * | 7 | OtherGenerator | RandomUuidGenerator, IncrementingUuidGenerator, + * OtherGenerator, YetAnotherGenerator | OtherGenerator used | + */ + @Test + void test_case_7() { + Options options = () -> OtherGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class, + YetAnotherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 8 | IncrementingUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | + * IncrementingUuidGenerator used | + */ + @Test + void test_case_8() { + Options options = () -> IncrementingUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class, + YetAnotherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(IncrementingUuidGenerator.class)); + } + + /** + * | 9 | IncrementingUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator | IncrementingUuidGenerator used | + */ + @Test + void test_case_9() { + Options options = () -> IncrementingUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + UuidGeneratorServiceLoaderTest.class::getClassLoader, + options); + assertThat(loader.loadUuidGenerator(), instanceOf(IncrementingUuidGenerator.class)); + } + + /** + * | 10 | OtherGenerator | none | exception, generator OtherGenerator not + * available | + */ + @Test + void test_case_10() { + + Options options = () -> OtherGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class), + options); + + CucumberException exception = assertThrows(CucumberException.class, loader::loadUuidGenerator); + assertThat(exception.getMessage(), is("" + + "Could not find UUID generator io.cucumber.core.runtime.UuidGeneratorServiceLoaderTest$OtherGenerator.\n" + + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "Has the class been registered with SPI and is it available on\n" + + "the classpath?")); + } + + /** + * | 11 | undefined | OtherGenerator | OtherGenerator used | + */ + @Test + void test_case_11() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + OtherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 12 | undefined | IncrementingUuidGenerator, OtherGenerator | + * OtherGenerator used | + */ + @Test + void test_case_12() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class), + options); + + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 13 | undefined | IncrementingUuidGenerator | IncrementingUuidGenerator + * used | + */ + @Test + void test_case_13() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + IncrementingUuidGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(IncrementingUuidGenerator.class)); + } + + public static class OtherGenerator implements UuidGenerator { + @Override + public UUID get() { + return null; + } + } + + public static class YetAnotherGenerator implements UuidGenerator { + @Override + public UUID get() { + return null; + } + } + +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md new file mode 100644 index 0000000000..34cad9beb1 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md @@ -0,0 +1,17 @@ +# Testcases for `UuidGeneratorServiceLoader` + +| # | uuid-generator property | Available services | Result | +|-----|---------------------------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| 1 | undefined | none | exception, no generators available | +| 2 | undefined | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | +| 3 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | +| 4 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | +| 5 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | RandomUuidGenerator used | +| 6 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | exception, cucumber couldn't decide multiple (non default) generators available | +| 7 | OtherGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | OtherGenerator used | +| 8 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | IncrementingUuidGenerator used | +| 9 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | IncrementingUuidGenerator used | +| 10 | OtherGenerator | none | exception, generator OtherGenerator not available | +| 11 | undefined | OtherGenerator | OtherGenerator used | +| 12 | undefined | IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | +| 13 | undefined | IncrementingUuidGenerator | IncrementingUuidGenerator used | diff --git a/cucumber-deltaspike/pom.xml b/cucumber-deltaspike/pom.xml index 0885894bf7..148b653ee3 100644 --- a/cucumber-deltaspike/pom.xml +++ b/cucumber-deltaspike/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-deltaspike diff --git a/cucumber-gherkin-messages/pom.xml b/cucumber-gherkin-messages/pom.xml index d2f80cc903..1011cdc84f 100644 --- a/cucumber-gherkin-messages/pom.xml +++ b/cucumber-gherkin-messages/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT diff --git a/cucumber-gherkin/pom.xml b/cucumber-gherkin/pom.xml index 2e5f8b9b3c..bc428c332b 100644 --- a/cucumber-gherkin/pom.xml +++ b/cucumber-gherkin/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT diff --git a/cucumber-guice/pom.xml b/cucumber-guice/pom.xml index 9918cfbca1..ae605315c8 100644 --- a/cucumber-guice/pom.xml +++ b/cucumber-guice/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-guice diff --git a/cucumber-jakarta-cdi/pom.xml b/cucumber-jakarta-cdi/pom.xml index 7896b6ac27..103a00208a 100644 --- a/cucumber-jakarta-cdi/pom.xml +++ b/cucumber-jakarta-cdi/pom.xml @@ -16,7 +16,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-jakarta-cdi diff --git a/cucumber-jakarta-openejb/pom.xml b/cucumber-jakarta-openejb/pom.xml index e8ef2c342d..50e41f64a6 100644 --- a/cucumber-jakarta-openejb/pom.xml +++ b/cucumber-jakarta-openejb/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-jakarta-openejb diff --git a/cucumber-java/pom.xml b/cucumber-java/pom.xml index c91c4f70d7..f540d9a468 100644 --- a/cucumber-java/pom.xml +++ b/cucumber-java/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-java diff --git a/cucumber-java8/pom.xml b/cucumber-java8/pom.xml index f4e9d1eea4..d168bb549c 100644 --- a/cucumber-java8/pom.xml +++ b/cucumber-java8/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-java8 diff --git a/cucumber-junit-platform-engine/pom.xml b/cucumber-junit-platform-engine/pom.xml index 709748aa3e..27e6e0438a 100644 --- a/cucumber-junit-platform-engine/pom.xml +++ b/cucumber-junit-platform-engine/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-junit-platform-engine diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java index 233d6909a5..aed3d7a449 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java @@ -172,6 +172,16 @@ public final class Constants { */ public static final String OBJECT_FACTORY_PROPERTY_NAME = io.cucumber.core.options.Constants.OBJECT_FACTORY_PROPERTY_NAME; + /** + * Property name to select custom UUID generator implementation: {@value} + *

+ * By default, if a single UUID generator is available on the class path + * that object factory will be used, or more than one UUID generator and the + * #RandomUuidGenerator are available on the classpath, the + * #RandomUuidGenerator will be used. + */ + public static final String UUID_GENERATOR_PROPERTY_NAME = io.cucumber.core.options.Constants.UUID_GENERATOR_PROPERTY_NAME; + /** * Property name to control naming convention for generated snippets: * {@value} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java index c72a2af41d..09d7a40f8a 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java @@ -1,11 +1,13 @@ package io.cucumber.junit.platform.engine; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.feature.GluePath; import io.cucumber.core.options.ObjectFactoryParser; import io.cucumber.core.options.PluginOption; import io.cucumber.core.options.SnippetTypeParser; +import io.cucumber.core.options.UuidGeneratorParser; import io.cucumber.core.plugin.NoPublishFormatter; import io.cucumber.core.plugin.PublishFormatter; import io.cucumber.core.snippets.SnippetType; @@ -39,11 +41,13 @@ import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.SNIPPET_TYPE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.UUID_GENERATOR_PROPERTY_NAME; class CucumberEngineOptions implements io.cucumber.core.plugin.Options, io.cucumber.core.runner.Options, - io.cucumber.core.backend.Options { + io.cucumber.core.backend.Options, + io.cucumber.core.eventbus.Options { private final ConfigurationParameters configurationParameters; @@ -156,6 +160,13 @@ public Class getObjectFactoryClass() { .orElse(null); } + @Override + public Class getUuidGeneratorClass() { + return configurationParameters + .get(UUID_GENERATOR_PROPERTY_NAME, UuidGeneratorParser::parseUuidGenerator) + .orElse(null); + } + boolean isParallelExecutionEnabled() { return configurationParameters .getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME) diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java index 3f1bdaf915..e1260daf2d 100644 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java @@ -1,10 +1,11 @@ package io.cucumber.junit.platform.engine; +import io.cucumber.core.backend.DefaultObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import io.cucumber.core.plugin.Options; import io.cucumber.core.snippets.SnippetType; import org.junit.jupiter.api.Test; import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.support.hierarchical.Node; import java.net.URI; @@ -150,7 +151,25 @@ void isParallelExecutionEnabled() { ConfigurationParameters absent = new MapConfigurationParameters( "some key", "some value"); assertFalse(new CucumberEngineOptions(absent).isParallelExecutionEnabled()); + } + + @Test + void objectFactory() { + ConfigurationParameters configurationParameters = new MapConfigurationParameters( + Constants.OBJECT_FACTORY_PROPERTY_NAME, + DefaultObjectFactory.class.getName()); + assertThat(new CucumberEngineOptions(configurationParameters).getObjectFactoryClass(), + is(DefaultObjectFactory.class)); } + @Test + void uuidGenerator() { + ConfigurationParameters configurationParameters = new MapConfigurationParameters( + Constants.UUID_GENERATOR_PROPERTY_NAME, + IncrementingUuidGenerator.class.getName()); + + assertThat(new CucumberEngineOptions(configurationParameters).getUuidGeneratorClass(), + is(IncrementingUuidGenerator.class)); + } } diff --git a/cucumber-junit/pom.xml b/cucumber-junit/pom.xml index ad37bb09fc..8142b58a0c 100644 --- a/cucumber-junit/pom.xml +++ b/cucumber-junit/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-junit diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java b/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java index 455470cb1e..98a3094f30 100644 --- a/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java +++ b/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java @@ -149,6 +149,17 @@ */ Class objectFactory() default NoObjectFactory.class; + /** + * Specify a custom ObjectFactory. + *

+ * In case a custom ObjectFactory is needed, the class can be specified + * here. A custom ObjectFactory might be needed when more granular control + * is needed over the dependency injection mechanism. + * + * @return an {@link io.cucumber.core.backend.ObjectFactory} implementation + */ + Class uuidGenerator() default NoUuidGenerator.class; + enum SnippetType { UNDERSCORE, CAMELCASE } diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java b/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java index ce7206026c..eafd59b962 100644 --- a/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java +++ b/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java @@ -1,6 +1,7 @@ package io.cucumber.junit; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; import io.cucumber.core.options.CucumberOptionsAnnotationParser; @@ -102,6 +103,10 @@ public Class objectFactory() { return (annotation.objectFactory() == NoObjectFactory.class) ? null : annotation.objectFactory(); } + @Override + public Class uuidGenerator() { + return (annotation.uuidGenerator() == NoUuidGenerator.class) ? null : annotation.uuidGenerator(); + } } } diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java b/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java new file mode 100644 index 0000000000..91f40e3fd7 --- /dev/null +++ b/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java @@ -0,0 +1,20 @@ +package io.cucumber.junit; + +import io.cucumber.core.eventbus.UuidGenerator; + +import java.util.UUID; + +/** + * This UUID generator does nothing. It is solely needed for marking purposes. + */ +final class NoUuidGenerator implements UuidGenerator { + + private NoUuidGenerator() { + // No need for instantiation + } + + @Override + public UUID get() { + return null; + } +} diff --git a/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java b/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java index 43b84959fb..515f554ec2 100644 --- a/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java +++ b/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java @@ -1,6 +1,7 @@ package io.cucumber.junit; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,6 +22,7 @@ void setUp() { void testObjectFactoryWhenNotSpecified() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithDefault.class); + assertNotNull(options); assertNull(options.objectFactory()); } @@ -28,10 +30,26 @@ void testObjectFactoryWhenNotSpecified() { void testObjectFactory() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithCustomObjectFactory.class); - assertNotNull(options.objectFactory()); + assertNotNull(options); assertEquals(TestObjectFactory.class, options.objectFactory()); } + @Test + void testUuidGeneratorWhenNotSpecified() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithDefault.class); + assertNotNull(options); + assertNull(options.uuidGenerator()); + } + + @Test + void testUuidGenerator() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithCustomUuidGenerator.class); + assertNotNull(options); + assertEquals(IncrementingUuidGenerator.class, options.uuidGenerator()); + } + @CucumberOptions() private static final class ClassWithDefault { @@ -42,6 +60,11 @@ private static final class ClassWithCustomObjectFactory { } + @CucumberOptions(uuidGenerator = IncrementingUuidGenerator.class) + private static final class ClassWithCustomUuidGenerator { + + } + private static final class TestObjectFactory implements ObjectFactory { @Override diff --git a/cucumber-kotlin-java8/pom.xml b/cucumber-kotlin-java8/pom.xml index b0e979f1d6..78575066df 100644 --- a/cucumber-kotlin-java8/pom.xml +++ b/cucumber-kotlin-java8/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-kotlin-java8 diff --git a/cucumber-openejb/pom.xml b/cucumber-openejb/pom.xml index 3935e8a197..b4078bc381 100644 --- a/cucumber-openejb/pom.xml +++ b/cucumber-openejb/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-openejb diff --git a/cucumber-picocontainer/pom.xml b/cucumber-picocontainer/pom.xml index 4ccc244368..269332be47 100644 --- a/cucumber-picocontainer/pom.xml +++ b/cucumber-picocontainer/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-picocontainer diff --git a/cucumber-plugin/pom.xml b/cucumber-plugin/pom.xml index f573fdeea9..1f58288b92 100644 --- a/cucumber-plugin/pom.xml +++ b/cucumber-plugin/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-plugin diff --git a/cucumber-spring/pom.xml b/cucumber-spring/pom.xml index 6f208692f9..84ede415c0 100644 --- a/cucumber-spring/pom.xml +++ b/cucumber-spring/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-spring diff --git a/cucumber-testng/pom.xml b/cucumber-testng/pom.xml index a023bcbc9d..00d83fa394 100644 --- a/cucumber-testng/pom.xml +++ b/cucumber-testng/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-testng diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java b/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java index 84d6abd366..9b7f9fbbea 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java @@ -122,6 +122,17 @@ */ Class objectFactory() default NoObjectFactory.class; + /** + * Specify a custom ObjectFactory. + *

+ * In case a custom ObjectFactory is needed, the class can be specified + * here. A custom ObjectFactory might be needed when more granular control + * is needed over the dependency injection mechanism. + * + * @return an {@link io.cucumber.core.backend.ObjectFactory} implementation + */ + Class uuidGenerator() default NoUuidGenerator.class; + enum SnippetType { UNDERSCORE, CAMELCASE } diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java b/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java new file mode 100644 index 0000000000..25a4b6759b --- /dev/null +++ b/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java @@ -0,0 +1,20 @@ +package io.cucumber.testng; + +import io.cucumber.core.eventbus.UuidGenerator; + +import java.util.UUID; + +/** + * This UUID generator does nothing. It is solely needed for marking purposes. + */ +final class NoUuidGenerator implements UuidGenerator { + + private NoUuidGenerator() { + // No need for instantiation + } + + @Override + public UUID get() { + return null; + } +} diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java b/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java index ed8218f537..52d1b81a1a 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java @@ -1,6 +1,7 @@ package io.cucumber.testng; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; import io.cucumber.core.options.CucumberOptionsAnnotationParser; @@ -102,6 +103,10 @@ public Class objectFactory() { return (annotation.objectFactory() == NoObjectFactory.class) ? null : annotation.objectFactory(); } + @Override + public Class uuidGenerator() { + return (annotation.uuidGenerator() == NoUuidGenerator.class) ? null : annotation.uuidGenerator(); + } } } diff --git a/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java b/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java index c7b4ccc8c2..40045bd841 100644 --- a/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java +++ b/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java @@ -1,6 +1,7 @@ package io.cucumber.testng; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -21,6 +22,7 @@ void setUp() { void testObjectFactoryWhenNotSpecified() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithDefault.class); + assertNotNull(options); assertNull(options.objectFactory()); } @@ -28,10 +30,26 @@ void testObjectFactoryWhenNotSpecified() { void testObjectFactory() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithCustomObjectFactory.class); - assertNotNull(options.objectFactory()); + assertNotNull(options); assertEquals(TestObjectFactory.class, options.objectFactory()); } + @Test + void testUuidGeneratorWhenNotSpecified() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithDefault.class); + assertNotNull(options); + assertNull(options.uuidGenerator()); + } + + @Test + void testUuidGenerator() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithCustomUuidGenerator.class); + assertNotNull(options); + assertEquals(IncrementingUuidGenerator.class, options.uuidGenerator()); + } + @CucumberOptions() private static final class ClassWithDefault { @@ -42,6 +60,11 @@ private static final class ClassWithCustomObjectFactory { } + @CucumberOptions(uuidGenerator = IncrementingUuidGenerator.class) + private static final class ClassWithCustomUuidGenerator { + + } + private static final class TestObjectFactory implements ObjectFactory { @Override diff --git a/datatable-matchers/pom.xml b/datatable-matchers/pom.xml index 659b66be80..1aedc0c903 100644 --- a/datatable-matchers/pom.xml +++ b/datatable-matchers/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT datatable-matchers diff --git a/datatable/pom.xml b/datatable/pom.xml index 315a1fefc0..dceb876af8 100644 --- a/datatable/pom.xml +++ b/datatable/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT datatable diff --git a/docstring/pom.xml b/docstring/pom.xml index 26c12cb1cc..7606517d44 100644 --- a/docstring/pom.xml +++ b/docstring/pom.xml @@ -3,7 +3,7 @@ cucumber-jvm io.cucumber - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT 4.0.0 diff --git a/examples/calculator-java-cli/pom.xml b/examples/calculator-java-cli/pom.xml index 0bba7e28e1..82c51bd6e5 100644 --- a/examples/calculator-java-cli/pom.xml +++ b/examples/calculator-java-cli/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-cli diff --git a/examples/calculator-java-junit4/pom.xml b/examples/calculator-java-junit4/pom.xml index dff7ea4334..0adaf774d2 100644 --- a/examples/calculator-java-junit4/pom.xml +++ b/examples/calculator-java-junit4/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-junit4 diff --git a/examples/calculator-java-junit5/pom.xml b/examples/calculator-java-junit5/pom.xml index d70df8cbd5..ca450299e3 100644 --- a/examples/calculator-java-junit5/pom.xml +++ b/examples/calculator-java-junit5/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-junit5 diff --git a/examples/calculator-java-testng/pom.xml b/examples/calculator-java-testng/pom.xml index 768e375ffc..46d5f22a6b 100644 --- a/examples/calculator-java-testng/pom.xml +++ b/examples/calculator-java-testng/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-testng diff --git a/examples/calculator-java8-cli/pom.xml b/examples/calculator-java8-cli/pom.xml index e941f496ac..c6c8bb3df3 100644 --- a/examples/calculator-java8-cli/pom.xml +++ b/examples/calculator-java8-cli/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java8-cli diff --git a/examples/pom.xml b/examples/pom.xml index 0e16a909a6..08f2bed077 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT examples diff --git a/examples/spring-java-junit5/pom.xml b/examples/spring-java-junit5/pom.xml index 21026bdc1c..f9b23501c7 100644 --- a/examples/spring-java-junit5/pom.xml +++ b/examples/spring-java-junit5/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT spring-java-junit5 diff --git a/examples/wicket-java-junit4/pom.xml b/examples/wicket-java-junit4/pom.xml index bd2b005cca..b00a8c9311 100644 --- a/examples/wicket-java-junit4/pom.xml +++ b/examples/wicket-java-junit4/pom.xml @@ -3,7 +3,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT wicket-java-junit4 pom diff --git a/examples/wicket-java-junit4/wicket-main/pom.xml b/examples/wicket-java-junit4/wicket-main/pom.xml index c072b47901..e2c331d77c 100644 --- a/examples/wicket-java-junit4/wicket-main/pom.xml +++ b/examples/wicket-java-junit4/wicket-main/pom.xml @@ -3,7 +3,7 @@ io.cucumber wicket-java-junit4 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT wicket-main Examples: Wicket application diff --git a/examples/wicket-java-junit4/wicket-test/pom.xml b/examples/wicket-java-junit4/wicket-test/pom.xml index bd2aa7ce1f..5e42ba5ac0 100644 --- a/examples/wicket-java-junit4/wicket-test/pom.xml +++ b/examples/wicket-java-junit4/wicket-test/pom.xml @@ -3,7 +3,7 @@ io.cucumber wicket-java-junit4 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT wicket-test Examples: Wicket application tested with Selenium diff --git a/pom.xml b/pom.xml index cbc45d574b..96cf0ed92a 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT pom Cucumber-JVM Cucumber for the JVM @@ -18,7 +18,7 @@ 1.8 8 - 1674814830 + 1678784521 scm:git:git://github.com/cucumber/cucumber-jvm.git From db69d23234b47a87e1475c6ddb3d97dcf5e9300b Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Tue, 14 Mar 2023 08:02:21 +0100 Subject: [PATCH 2/8] feat: added UUID generator selection through SPI for https://github.com/cucumber/cucumber-jvm/issues/2698 --- .revapi/api-changes.json | 24 ++ CHANGELOG.md | 3 + compatibility/pom.xml | 2 +- cucumber-archetype/pom.xml | 2 +- cucumber-bom/pom.xml | 40 +-- cucumber-cdi2/pom.xml | 2 +- cucumber-core/README.md | 20 ++ cucumber-core/pom.xml | 2 +- .../cucumber/core/cli/CommandlineOptions.java | 2 + .../eventbus/IncrementingUuidGenerator.java | 40 +++ .../io/cucumber/core/eventbus/Options.java | 7 + .../core/eventbus/RandomUuidGenerator.java | 14 + .../cucumber/core/eventbus/UuidGenerator.java | 13 + .../options/CommandlineOptionsParser.java | 5 + .../io/cucumber/core/options/Constants.java | 9 + .../CucumberOptionsAnnotationParser.java | 10 + .../options/CucumberPropertiesParser.java | 6 + .../cucumber/core/options/RuntimeOptions.java | 14 +- .../core/options/RuntimeOptionsBuilder.java | 11 + .../core/options/UuidGeneratorParser.java | 27 ++ .../java/io/cucumber/core/runner/Options.java | 3 + .../io/cucumber/core/runtime/Runtime.java | 10 +- .../runtime/UuidGeneratorServiceLoader.java | 130 +++++++++ .../io.cucumber.core.eventbus.UuidGenerator | 2 + .../io/cucumber/core/options/USAGE.txt | 7 + .../IncrementingUuidGeneratorTest.java | 62 ++++ .../eventbus/RandomUuidGeneratorTest.java | 24 ++ .../options/CommandlineOptionsParserTest.java | 13 +- .../core/options/CucumberOptions.java | 2 + .../CucumberOptionsAnnotationParserTest.java | 18 ++ .../core/options/CucumberPropertiesTest.java | 9 +- .../core/options/NoUuidGenerator.java | 20 ++ .../options/RuntimeOptionsBuilderTest.java | 22 ++ .../core/options/UuidGeneratorParserTest.java | 53 ++++ .../ObjectFactoryServiceLoaderTest.java | 171 ++++++++--- .../runtime/ObjectFactoryServiceLoaderTest.md | 18 ++ .../runtime/ServiceLoaderTestClassLoader.java | 96 +++++++ .../UuidGeneratorServiceLoaderTest.java | 268 ++++++++++++++++++ .../runtime/UuidGeneratorServiceLoaderTest.md | 17 ++ cucumber-deltaspike/pom.xml | 2 +- cucumber-gherkin-messages/pom.xml | 2 +- cucumber-gherkin/pom.xml | 2 +- cucumber-guice/pom.xml | 2 +- cucumber-jakarta-cdi/pom.xml | 2 +- cucumber-jakarta-openejb/pom.xml | 2 +- cucumber-java/pom.xml | 2 +- cucumber-java8/pom.xml | 2 +- cucumber-junit-platform-engine/pom.xml | 2 +- .../junit/platform/engine/Constants.java | 10 + .../engine/CucumberEngineOptions.java | 13 +- .../engine/CucumberEngineOptionsTest.java | 21 +- cucumber-junit/pom.xml | 2 +- .../io/cucumber/junit/CucumberOptions.java | 11 + .../junit/JUnitCucumberOptionsProvider.java | 5 + .../io/cucumber/junit/NoUuidGenerator.java | 20 ++ .../JUnitCucumberOptionsProviderTest.java | 25 +- cucumber-kotlin-java8/pom.xml | 2 +- cucumber-openejb/pom.xml | 2 +- cucumber-picocontainer/pom.xml | 2 +- cucumber-plugin/pom.xml | 2 +- cucumber-spring/pom.xml | 2 +- cucumber-testng/pom.xml | 2 +- .../io/cucumber/testng/CucumberOptions.java | 11 + .../io/cucumber/testng/NoUuidGenerator.java | 20 ++ .../testng/TestNGCucumberOptionsProvider.java | 5 + .../TestNGCucumberOptionsProviderTest.java | 25 +- datatable-matchers/pom.xml | 2 +- datatable/pom.xml | 2 +- docstring/pom.xml | 2 +- examples/calculator-java-cli/pom.xml | 2 +- examples/calculator-java-junit4/pom.xml | 2 +- examples/calculator-java-junit5/pom.xml | 2 +- examples/calculator-java-testng/pom.xml | 2 +- examples/calculator-java8-cli/pom.xml | 2 +- examples/pom.xml | 2 +- examples/spring-java-junit5/pom.xml | 2 +- examples/wicket-java-junit4/pom.xml | 2 +- .../wicket-java-junit4/wicket-main/pom.xml | 2 +- .../wicket-java-junit4/wicket-test/pom.xml | 2 +- pom.xml | 4 +- 80 files changed, 1320 insertions(+), 106 deletions(-) create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java create mode 100644 cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator create mode 100644 cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java create mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md create mode 100644 cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java create mode 100644 cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java diff --git a/.revapi/api-changes.json b/.revapi/api-changes.json index 5bd2911d71..4355ce9dd1 100644 --- a/.revapi/api-changes.json +++ b/.revapi/api-changes.json @@ -181,6 +181,18 @@ "code": "java.method.finalMethodAddedToNonFinalClass", "new": "method java.lang.Long io.cucumber.core.internal.com.fasterxml.jackson.databind.deser.std.StdDeserializer::_parseLong(io.cucumber.core.internal.com.fasterxml.jackson.databind.DeserializationContext, java.lang.String) throws java.io.IOException", "justification": "Internal API" + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method java.lang.Class io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions::uuidGenerator()", + "justification": "Internal API" + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method java.lang.Class io.cucumber.core.runner.Options::getUuidGeneratorClass()", + "justification": "Internal API" } ] } @@ -331,6 +343,12 @@ "code": "java.method.defaultMethodAddedToInterface", "new": "method java.util.Set org.testng.ITestNGMethod::upstreamDependencies()", "justification": "Third party api change" + }, + { + "ignore": true, + "code": "java.class.externalClassExposedInAPI", + "new": "interface io.cucumber.core.eventbus.UuidGenerator", + "justification": "Part of cucumber API" } ] } @@ -383,6 +401,12 @@ "new": "method int org.junit.platform.engine.ConfigurationParameters::size()", "annotation": "@org.apiguardian.api.API(status = org.apiguardian.api.API.Status.DEPRECATED, since = \"1.9\")", "justification": "API consumed from JUnit 5" + }, + { + "ignore": true, + "code": "java.class.externalClassExposedInAPI", + "new": "interface io.cucumber.core.eventbus.UuidGenerator", + "justification": "Part of cucumber API" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac08ee839..0aeb6dbd72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [Core] Improved event bus performance using UUID generator selectable through SPI ([#2703](https://github.com/cucumber/cucumber-jvm/pull/2703) Julien Kronegg) + ## [7.11.1] - 2023-01-27 ### Added - [Core] Warn when `cucumber.options` is used ([#2685](https://github.com/cucumber/cucumber-jvm/pull/2685) M.P. Korstanje) diff --git a/compatibility/pom.xml b/compatibility/pom.xml index 9d62f76276..48fd6e00f0 100644 --- a/compatibility/pom.xml +++ b/compatibility/pom.xml @@ -4,7 +4,7 @@ cucumber-jvm io.cucumber - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT 4.0.0 diff --git a/cucumber-archetype/pom.xml b/cucumber-archetype/pom.xml index 55fc46e754..5c5f951bd9 100644 --- a/cucumber-archetype/pom.xml +++ b/cucumber-archetype/pom.xml @@ -6,7 +6,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-archetype diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index 78654cf628..f19b258c37 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -3,7 +3,7 @@ cucumber-jvm io.cucumber - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT 4.0.0 pom @@ -63,97 +63,97 @@ io.cucumber cucumber-cdi2 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-core - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber datatable - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber datatable-matchers - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-deltaspike - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber docstring - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-gherkin - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-gherkin-messages - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-guice - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-jakarta-cdi - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-java - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-java8 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-junit - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-junit-platform-engine - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-openejb - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-picocontainer - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-plugin - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-spring - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT io.cucumber cucumber-testng - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT diff --git a/cucumber-cdi2/pom.xml b/cucumber-cdi2/pom.xml index 24debd11b4..257cb96af0 100644 --- a/cucumber-cdi2/pom.xml +++ b/cucumber-cdi2/pom.xml @@ -14,7 +14,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-cdi2 diff --git a/cucumber-core/README.md b/cucumber-core/README.md index cdd5a7824f..6e07801ada 100644 --- a/cucumber-core/README.md +++ b/cucumber-core/README.md @@ -52,6 +52,9 @@ cucumber.plugin= # comma separated plugin strings. cucumber.object-factory= # object factory class name. # example: com.example.MyObjectFactory +cucumber.uuid-generator= # UUID generator class name. + # example: com.example.MyUuidGenerator + cucumber.publish.enabled # true or false. default: false # enable publishing of test results @@ -79,6 +82,23 @@ They are respectively responsible for discovering glue classes, registering step definitions, and creating instances of said glue classes. Backend and object factory implementations are discovered via SPI. +## Event bus ## + +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: + +| 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 | + +The performance gain on real project depend on the feature size. + +When not specified, the `RandomUuidGenerator` is used. + ## Plugin ## By implementing the Plugin interface classes can listen to execution events diff --git a/cucumber-core/pom.xml b/cucumber-core/pom.xml index bc7aaac6f8..d246466ee0 100644 --- a/cucumber-core/pom.xml +++ b/cucumber-core/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-core diff --git a/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java b/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java index 229697b122..3d9ea17d7d 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java @@ -66,6 +66,8 @@ public final class CommandlineOptions { public static final String OBJECT_FACTORY = "--object-factory"; + public static final String UUID_GENERATOR = "--uuid-generator"; + private CommandlineOptions() { } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java new file mode 100644 index 0000000000..35fa782e79 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java @@ -0,0 +1,40 @@ +package io.cucumber.core.eventbus; + +import io.cucumber.core.exception.CucumberException; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 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. If you use Cucumber in + * multi-JVM setup, you should use #RandomUuidGenerator instead. Note that the + * UUID version and variant is not guaranteed to be stable. + */ +public class IncrementingUuidGenerator implements UuidGenerator { + private static final AtomicLong sessionCounter = new AtomicLong(Long.MIN_VALUE); + + private final long sessionId; + private final AtomicLong counter = new AtomicLong(Long.MIN_VALUE); + + public IncrementingUuidGenerator() { + sessionId = sessionCounter.incrementAndGet(); + } + + /** + * Generate a new UUID. Will throw an exception when out of capacity. + * + * @return a non-null UUID + * @throws CucumberException when out of capacity + */ + @Override + public UUID get() { + long leastSigBits = counter.incrementAndGet(); + if (leastSigBits == Long.MAX_VALUE) { + throw new CucumberException( + "Out of IncrementingUuidGenerator capacity. Please use the RandomUuidGenerator instead."); + } + return new UUID(sessionId, leastSigBits); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java new file mode 100644 index 0000000000..b14ef7a05e --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/Options.java @@ -0,0 +1,7 @@ +package io.cucumber.core.eventbus; + +public interface Options { + + Class getUuidGeneratorClass(); + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java new file mode 100644 index 0000000000..6ecdf84ac2 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java @@ -0,0 +1,14 @@ +package io.cucumber.core.eventbus; + +import java.util.UUID; + +/** + * UUID generator based on random numbers. The generator is thread-safe and + * supports multi-jvm usage of Cucumber. + */ +public class RandomUuidGenerator implements UuidGenerator { + @Override + public UUID get() { + return UUID.randomUUID(); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java new file mode 100644 index 0000000000..6557576ee4 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java @@ -0,0 +1,13 @@ +package io.cucumber.core.eventbus; + +import org.apiguardian.api.API; + +import java.util.UUID; +import java.util.function.Supplier; + +/** + * SPI (Service Provider Interface) to generate UUIDs. + */ +@API(status = API.Status.STABLE) +public interface UuidGenerator extends Supplier { +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java index ffa3502a1f..88db18e236 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java @@ -53,12 +53,14 @@ import static io.cucumber.core.cli.CommandlineOptions.TAGS; import static io.cucumber.core.cli.CommandlineOptions.TAGS_SHORT; import static io.cucumber.core.cli.CommandlineOptions.THREADS; +import static io.cucumber.core.cli.CommandlineOptions.UUID_GENERATOR; import static io.cucumber.core.cli.CommandlineOptions.VERSION; import static io.cucumber.core.cli.CommandlineOptions.VERSION_SHORT; import static io.cucumber.core.cli.CommandlineOptions.WIP; import static io.cucumber.core.cli.CommandlineOptions.WIP_SHORT; import static io.cucumber.core.options.ObjectFactoryParser.parseObjectFactory; import static io.cucumber.core.options.OptionsFileParser.parseFeatureWithLinesFile; +import static io.cucumber.core.options.UuidGeneratorParser.parseUuidGenerator; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; @@ -167,6 +169,9 @@ private RuntimeOptionsBuilder parse(List args) { } else if (arg.equals(OBJECT_FACTORY)) { String objectFactoryClassName = removeArgFor(arg, args); parsedOptions.setObjectFactoryClass(parseObjectFactory(objectFactoryClassName)); + } else if (arg.equals(UUID_GENERATOR)) { + String uuidGeneratorClassName = removeArgFor(arg, args); + parsedOptions.setUuidGeneratorClass(parseUuidGenerator(uuidGeneratorClassName)); } else if (arg.startsWith("-")) { out.println("Unknown option: " + arg); printUsage(); diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java index dc88f3c281..8681e83d91 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.runtime.ObjectFactoryServiceLoader; +import io.cucumber.core.runtime.UuidGeneratorServiceLoader; public final class Constants { @@ -118,6 +119,14 @@ public final class Constants { */ public static final String OBJECT_FACTORY_PROPERTY_NAME = "cucumber.object-factory"; + /** + * Property name used to select a specific UUID generator implementation: + * {@value} + * + * @see UuidGeneratorServiceLoader + */ + public static final String UUID_GENERATOR_PROPERTY_NAME = "cucumber.uuid-generator"; + /** * Property name formerly used to pass command line options to Cucumber: * {@value} This property is no longer read by Cucumber. Please use any of diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java index 684af34c2d..576ac6ff07 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.feature.GluePath; @@ -45,6 +46,7 @@ public RuntimeOptionsBuilder parse(Class clazz) { addGlue(options, args); addFeatures(options, args); addObjectFactory(options, args); + addUuidGenerator(options, args); } } @@ -149,6 +151,12 @@ private void addObjectFactory(CucumberOptions options, RuntimeOptionsBuilder arg } } + private void addUuidGenerator(CucumberOptions options, RuntimeOptionsBuilder args) { + if (options.uuidGenerator() != null) { + args.setUuidGeneratorClass(options.uuidGenerator()); + } + } + private void addDefaultFeaturePathIfNoFeaturePathIsSpecified(RuntimeOptionsBuilder args, Class clazz) { if (!featuresSpecified) { String packageName = packagePath(clazz); @@ -208,6 +216,8 @@ public interface CucumberOptions { Class objectFactory(); + Class uuidGenerator(); + } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java index db2aab247f..d4bb7d2aaf 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java @@ -32,6 +32,7 @@ import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME; import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME; import static io.cucumber.core.options.Constants.SNIPPET_TYPE_PROPERTY_NAME; +import static io.cucumber.core.options.Constants.UUID_GENERATOR_PROPERTY_NAME; import static io.cucumber.core.options.Constants.WIP_PROPERTY_NAME; import static io.cucumber.core.options.OptionsFileParser.parseFeatureWithLinesFile; import static java.util.Arrays.stream; @@ -102,6 +103,11 @@ public RuntimeOptionsBuilder parse(CucumberPropertiesProvider properties) { ObjectFactoryParser::parseObjectFactory, builder::setObjectFactoryClass); + parse(properties, + UUID_GENERATOR_PROPERTY_NAME, + UuidGeneratorParser::parseUuidGenerator, + builder::setUuidGeneratorClass); + parse(properties, OPTIONS_PROPERTY_NAME, identity(), diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java index 0c0490db5a..4db93daac4 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.order.PickleOrder; import io.cucumber.core.order.StandardPickleOrders; @@ -33,7 +34,8 @@ public final class RuntimeOptions implements io.cucumber.core.runner.Options, io.cucumber.core.plugin.Options, io.cucumber.core.filter.Options, - io.cucumber.core.backend.Options { + io.cucumber.core.backend.Options, + io.cucumber.core.eventbus.Options { private final List glue = new ArrayList<>(); private final List tagExpressions = new ArrayList<>(); @@ -48,6 +50,7 @@ public final class RuntimeOptions implements private PickleOrder pickleOrder = StandardPickleOrders.lexicalUriOrder(); private int count = 0; private Class objectFactoryClass; + private Class uuidGeneratorClass; private String publishToken; private boolean publish; private boolean publishQuiet; @@ -158,6 +161,15 @@ void setObjectFactoryClass(Class objectFactoryClass) { this.objectFactoryClass = objectFactoryClass; } + @Override + public Class getUuidGeneratorClass() { + return uuidGeneratorClass; + } + + void setUuidGeneratorClass(Class uuidGeneratorClass) { + this.uuidGeneratorClass = uuidGeneratorClass; + } + void setSnippetType(SnippetType snippetType) { this.snippetType = snippetType; } diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java index db7bddafa5..58b6792bc5 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.order.PickleOrder; @@ -31,6 +32,7 @@ public final class RuntimeOptionsBuilder { private PickleOrder parsedPickleOrder = null; private Integer parsedCount = null; private Class parsedObjectFactoryClass = null; + private Class parsedUuidGeneratorClass = null; private Boolean addDefaultSummaryPrinter = null; private boolean addDefaultGlueIfAbsent; private boolean addDefaultFeaturePathIfAbsent; @@ -132,6 +134,10 @@ public RuntimeOptions build(RuntimeOptions runtimeOptions) { runtimeOptions.setObjectFactoryClass(parsedObjectFactoryClass); } + if (parsedUuidGeneratorClass != null) { + runtimeOptions.setUuidGeneratorClass(parsedUuidGeneratorClass); + } + if (addDefaultSummaryPrinter != null && addDefaultSummaryPrinter) { runtimeOptions.addDefaultSummaryPrinter(); } @@ -239,6 +245,11 @@ public RuntimeOptionsBuilder setObjectFactoryClass(Class uuidGeneratorClass) { + this.parsedUuidGeneratorClass = uuidGeneratorClass; + return this; + } + public RuntimeOptionsBuilder setPublishToken(String token) { this.parsedPublishToken = token; return this; diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java new file mode 100644 index 0000000000..ecc633f211 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/options/UuidGeneratorParser.java @@ -0,0 +1,27 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.UuidGenerator; + +public final class UuidGeneratorParser { + + private UuidGeneratorParser() { + + } + + @SuppressWarnings("unchecked") + public static Class parseUuidGenerator(String cucumberUuidGenerator) { + Class uuidGeneratorClass; + try { + uuidGeneratorClass = Class.forName(cucumberUuidGenerator); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + String.format("Could not load UUID generator class for '%s'", cucumberUuidGenerator), e); + } + if (!UuidGenerator.class.isAssignableFrom(uuidGeneratorClass)) { + throw new IllegalArgumentException(String.format("UUID generator class '%s' was not a subclass of '%s'", + uuidGeneratorClass, UuidGenerator.class)); + } + return (Class) uuidGeneratorClass; + } + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java index a1094523c5..0fe0ffb807 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java @@ -1,6 +1,7 @@ package io.cucumber.core.runner; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.snippets.SnippetType; import java.net.URI; @@ -16,4 +17,6 @@ public interface Options { Class getObjectFactoryClass(); + Class getUuidGeneratorClass(); + } diff --git a/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java b/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java index 1a7b92c23f..8fba565bcb 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runtime/Runtime.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.UUID; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -138,7 +137,7 @@ public byte exitStatus() { public static class Builder { - private EventBus eventBus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + private EventBus eventBus; private Supplier classLoader = ClassLoaders::getDefaultClassLoader; private RuntimeOptions runtimeOptions = RuntimeOptions.defaultOptions(); private BackendSupplier backendSupplier; @@ -197,6 +196,13 @@ public Runtime build() { final ExitStatus exitStatus = new ExitStatus(runtimeOptions); plugins.addPlugin(exitStatus); + if (this.eventBus == null) { + final UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader( + classLoader, + runtimeOptions); + this.eventBus = new TimeServiceEventBus(Clock.systemUTC(), + uuidGeneratorServiceLoader.loadUuidGenerator()); + } final EventBus eventBus = synchronize(this.eventBus); if (runtimeOptions.isMultiThreaded()) { diff --git a/cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java b/cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java new file mode 100644 index 0000000000..d8fbb2baf2 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/runtime/UuidGeneratorServiceLoader.java @@ -0,0 +1,130 @@ +package io.cucumber.core.runtime; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.Options; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; +import io.cucumber.core.exception.CucumberException; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; + +/** + * Loads an instance of {@link UuidGenerator} using the {@link ServiceLoader} + * mechanism. + *

+ * Will load an instance of the class provided by + * {@link Options#getUuidGeneratorClass()}. If + * {@link Options#getUuidGeneratorClass()} does not provide a class, if there is + * exactly one {@code UuidGenerator} instance available that instance will be + * used. + *

+ * Otherwise {@link RandomUuidGenerator} with no dependency injection + */ +public final class UuidGeneratorServiceLoader { + + private final Supplier classLoaderSupplier; + private final Options options; + + public UuidGeneratorServiceLoader(Supplier classLoaderSupplier, Options options) { + this.classLoaderSupplier = requireNonNull(classLoaderSupplier); + this.options = requireNonNull(options); + } + + UuidGenerator loadUuidGenerator() { + Class objectFactoryClass = options.getUuidGeneratorClass(); + ClassLoader classLoader = classLoaderSupplier.get(); + ServiceLoader loader = ServiceLoader.load(UuidGenerator.class, classLoader); + if (objectFactoryClass == null) { + return loadSingleUuidGeneratorOrDefault(loader); + } + + return loadSelectedUuidGenerator(loader, objectFactoryClass); + } + + private static UuidGenerator loadSingleUuidGeneratorOrDefault(ServiceLoader loader) { + Iterator uuidGenerators = loader.iterator(); + + // categorize the UUID generators (random, incrementing or external) + UuidGenerator randomGenerator = null; + UuidGenerator incrementingGenerator = null; + UuidGenerator externalGenerator = null; + while (uuidGenerators.hasNext()) { + UuidGenerator uuidGenerator = uuidGenerators.next(); + if (uuidGenerator instanceof RandomUuidGenerator) { + randomGenerator = uuidGenerator; + } else if (uuidGenerator instanceof IncrementingUuidGenerator) { + incrementingGenerator = uuidGenerator; + } else { + if (externalGenerator != null) { + // we have multiple external generators, which is an error + throw new CucumberException(getMultipleUuidGeneratorLogMessage( + Arrays.asList(externalGenerator, uuidGenerator))); + } + externalGenerator = uuidGenerator; + } + } + + // decide which generator to use + if (externalGenerator != null) { + // we have a single external generator + return externalGenerator; + } else if (randomGenerator != null) { + // we don't have any external generators, use random if available + return randomGenerator; + } else if (incrementingGenerator != null) { + // we don't have any external generators and no random, use + // incrementing if available + return incrementingGenerator; + } else { + // we don't have any generators at all, throw an error + throw new CucumberException("" + + "Could not find any UUID generator.\n" + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "This typically happens when using shaded jars. Make sure\n" + + "to merge all SPI definitions in META-INF/services correctly"); + } + } + + private static UuidGenerator loadSelectedUuidGenerator( + ServiceLoader loader, + Class uuidGeneratorClass + ) { + for (UuidGenerator uuidGenerator : loader) { + if (uuidGeneratorClass.equals(uuidGenerator.getClass())) { + return uuidGenerator; + } + } + + throw new CucumberException("" + + "Could not find UUID generator " + uuidGeneratorClass.getName() + ".\n" + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "Has the class been registered with SPI and is it available on\n" + + "the classpath?"); + } + + private static String getMultipleUuidGeneratorLogMessage(List uuidGenerators) { + String factoryNames = Stream.of(uuidGenerators) + .map(Object::getClass) + .map(Class::getName) + .collect(Collectors.joining(", ")); + + return "More than one Cucumber UuidGenerator was found on the classpath\n" + + "\n" + + "Found: " + factoryNames + "\n" + + "\n" + + "You can either remove the unnecessary SPI dependencies from your classpath\n" + + "or use the `cucumber.uuid-generator` property\n" + + "or `@CucumberOptions(uuidGenerator=...)` to select one UUID generator.\n"; + } + +} diff --git a/cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator b/cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator new file mode 100644 index 0000000000..c7c37e3f7b --- /dev/null +++ b/cucumber-core/src/main/resources/META-INF/services/io.cucumber.core.eventbus.UuidGenerator @@ -0,0 +1,2 @@ +io.cucumber.core.eventbus.RandomUuidGenerator +io.cucumber.core.eventbus.IncrementingUuidGenerator diff --git a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt index 58a57584b1..17a05569ba 100644 --- a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt +++ b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt @@ -73,6 +73,13 @@ Options: be specified in: META-INF/services/io.cucumber.core.backend.ObjectFactory + --uuid-generator CLASSNAME Uses the class specified by CLASSNAME + as UUID generator. Be aware that the + class is loaded through a service + loader and therefore also needs to + be specified in: + META-INF/services/io.cucumber.core.eventbus.UuidGenerator + Feature path examples: When no feature path is provided cucumber will scan the classpath root diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java new file mode 100644 index 0000000000..a3fab341f1 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java @@ -0,0 +1,62 @@ +package io.cucumber.core.eventbus; + +import io.cucumber.core.exception.CucumberException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class IncrementingUuidGeneratorTest { + @Test + void generates_different_non_null_uuids() { + // Given + UuidGenerator generator = new IncrementingUuidGenerator(); + UUID uuid1 = generator.get(); + + // When + UUID uuid2 = generator.get(); + + // Then + assertNotNull(uuid1); + assertNotNull(uuid2); + assertNotEquals(uuid1, uuid2); + } + + @Test + void raises_exception_when_out_of_range() throws NoSuchFieldException, IllegalAccessException { + // Given + UuidGenerator generator = new IncrementingUuidGenerator(); + Field counterField = IncrementingUuidGenerator.class.getDeclaredField("counter"); + counterField.setAccessible(true); + AtomicLong counter = (AtomicLong) counterField.get(generator); + counter.set(Long.MAX_VALUE - 1); + + // When + CucumberException cucumberException = assertThrows(CucumberException.class, generator::get); + + // Then + assertThat(cucumberException.getMessage(), + Matchers.containsString("Out of IncrementingUuidGenerator capacity")); + } + + @Test + void same_thread_generates_different_UuidGenerators() { + // Given + UuidGenerator generator1 = new IncrementingUuidGenerator(); + UuidGenerator generator2 = new IncrementingUuidGenerator(); + + // When + UUID uuid1 = generator1.get(); + UUID uuid2 = generator2.get(); + + // Then + assertNotNull(uuid1); + assertNotNull(uuid2); + assertNotEquals(uuid1, uuid2); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java new file mode 100644 index 0000000000..c578c04d1e --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java @@ -0,0 +1,24 @@ +package io.cucumber.core.eventbus; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class RandomUuidGeneratorTest { + @Test + void generates_different_non_null_uuids() { + // Given + UuidGenerator generator = new RandomUuidGenerator(); + UUID uuid1 = generator.get(); + + // When + UUID uuid2 = generator.get(); + + // Then + assertNotNull(uuid1); + assertNotNull(uuid2); + assertNotEquals(uuid1, uuid2); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java index 74824c90aa..5d31229ae3 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java @@ -1,6 +1,7 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import io.cucumber.core.feature.TestFeatureParser; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.gherkin.Pickle; @@ -76,6 +77,16 @@ void testParseWithObjectFactoryArgument() { assertThat(options.getObjectFactoryClass(), Is.is(equalTo(TestObjectFactory.class))); } + @Test + void testParseWithUuidGeneratorArgument() { + RuntimeOptionsBuilder optionsBuilder = parser.parse("--uuid-generator", + IncrementingUuidGenerator.class.getName()); + assertNotNull(optionsBuilder); + RuntimeOptions options = optionsBuilder.build(); + assertNotNull(options); + assertThat(options.getUuidGeneratorClass(), Is.is(equalTo(IncrementingUuidGenerator.class))); + } + @Test void has_version_from_properties_file() { parser.parse("--version"); @@ -84,7 +95,7 @@ void has_version_from_properties_file() { } private String output() { - return new String(out.toByteArray(), StandardCharsets.UTF_8); + return out.toString(StandardCharsets.UTF_8); } @Test diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java index 267ac37c55..cf4457c7f2 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptions.java @@ -33,6 +33,8 @@ Class objectFactory() default NoObjectFactory.class; + Class uuidGenerator() default NoUuidGenerator.class; + String[] junit() default {}; } diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java index 43fb536e0b..e078ce47e2 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java @@ -1,6 +1,8 @@ package io.cucumber.core.options; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; import io.cucumber.core.plugin.HtmlFormatter; import io.cucumber.core.plugin.NoPublishFormatter; @@ -252,6 +254,13 @@ void cannot_create_with_glue_and_extra_glue() { is(equalTo("glue and extraGlue cannot be specified at the same time"))); } + @Test + void uuid_generator() { + RuntimeOptions runtimeOptions = parser().parse(ClassWithUuidGenerator.class).build(); + + assertThat(runtimeOptions.getUuidGeneratorClass(), is(IncrementingUuidGenerator.class)); + } + @CucumberOptions(snippets = SnippetType.CAMELCASE) private static class Snippets { // empty @@ -363,6 +372,11 @@ private static class ClassWithGlueAndExtraGlue { // empty } + @CucumberOptions(uuidGenerator = IncrementingUuidGenerator.class) + private static class ClassWithUuidGenerator extends ClassWithGlue { + // empty + } + private static class CoreCucumberOptions implements CucumberOptionsAnnotationParser.CucumberOptions { private final CucumberOptions annotation; @@ -426,6 +440,10 @@ public Class objectFactory() { return (annotation.objectFactory() == NoObjectFactory.class) ? null : annotation.objectFactory(); } + @Override + public Class uuidGenerator() { + return (annotation.uuidGenerator() == NoUuidGenerator.class) ? null : annotation.uuidGenerator(); + } } private static class CoreCucumberOptionsProvider implements CucumberOptionsAnnotationParser.OptionsProvider { diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java index a4f6087b76..85c5ee9385 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberPropertiesTest.java @@ -16,7 +16,14 @@ class CucumberPropertiesTest { @Test void looks_up_value_from_environment() { - assertThat(CucumberProperties.fromEnvironment().get("PATH"), is(notNullValue())); + Map properties = CucumberProperties.fromEnvironment(); + String path = properties.get("PATH"); + if (path == null) { + // on some Windows flavors, the PATH environment variable is named + // "Path" + path = properties.get("Path"); + } + assertThat(path, is(notNullValue())); } @Test diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java b/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java new file mode 100644 index 0000000000..a8ddb653b0 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java @@ -0,0 +1,20 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.UuidGenerator; + +import java.util.UUID; + +/** + * This UUID generator does nothing. It is solely needed for marking purposes. + */ +final class NoUuidGenerator implements UuidGenerator { + + private NoUuidGenerator() { + // No need for instantiation + } + + @Override + public UUID get() { + return null; + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java new file mode 100644 index 0000000000..02bb028bbf --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/options/RuntimeOptionsBuilderTest.java @@ -0,0 +1,22 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RuntimeOptionsBuilderTest { + + @Test + void build() { + // Given + RuntimeOptionsBuilder builder = new RuntimeOptionsBuilder() + .setUuidGeneratorClass(IncrementingUuidGenerator.class); + + // When + RuntimeOptions runtimeOptions = builder.build(); + + // Then + assertEquals(IncrementingUuidGenerator.class, runtimeOptions.getUuidGeneratorClass()); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java new file mode 100644 index 0000000000..012a2ec87c --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/options/UuidGeneratorParserTest.java @@ -0,0 +1,53 @@ +package io.cucumber.core.options; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class UuidGeneratorParserTest { + + @Test + void parseUuidGenerator_IncrementingUuidGenerator() { + // When + Class uuidGeneratorClass = UuidGeneratorParser + .parseUuidGenerator(IncrementingUuidGenerator.class.getName()); + + // Then + assertEquals(IncrementingUuidGenerator.class, uuidGeneratorClass); + } + + @Test + void parseUuidGenerator_RandomUuidGenerator() { + // When + Class uuidGeneratorClass = UuidGeneratorParser + .parseUuidGenerator(RandomUuidGenerator.class.getName()); + + // Then + assertEquals(RandomUuidGenerator.class, uuidGeneratorClass); + } + + @Test + void parseUuidGenerator_not_a_generator() { + // When + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> UuidGeneratorParser.parseUuidGenerator(String.class.getName())); + + // Then + assertThat(exception.getMessage(), Matchers.containsString("not a subclass")); + } + + @Test + void parseUuidGenerator_not_a_class() { + // When + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> UuidGeneratorParser.parseUuidGenerator("java.lang.NonExistingClassName")); + + // Then + assertThat(exception.getMessage(), Matchers.containsString("Could not load UUID generator class")); + } +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java index f6f5920a80..14580387cd 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java @@ -3,25 +3,50 @@ import io.cucumber.core.backend.DefaultObjectFactory; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.backend.Options; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.exception.CucumberException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; import java.util.function.Supplier; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; +/** + * @see test-cases description + */ class ObjectFactoryServiceLoaderTest { + /** + * Test case #1 + */ + @Test + void shouldThrowIfDefaultObjectFactoryServiceCouldNotBeLoaded() { + Options options = () -> null; + Supplier classLoader = () -> new ServiceLoaderTestClassLoader(ObjectFactory.class); + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + classLoader, + options); + + CucumberException exception = assertThrows(CucumberException.class, loader::loadObjectFactory); + assertThat(exception.getMessage(), is("" + + "Could not find any object factory.\n" + + "\n" + + "Cucumber uses SPI to discover object factory implementations.\n" + + "This typically happens when using shaded jars. Make sure\n" + + "to merge all SPI definitions in META-INF/services correctly")); + } + + /** + * Test case #2 + */ @Test void shouldLoadDefaultObjectFactoryService() { Options options = () -> null; @@ -31,6 +56,9 @@ void shouldLoadDefaultObjectFactoryService() { assertThat(loader.loadObjectFactory(), instanceOf(DefaultObjectFactory.class)); } + /** + * Test case #3 + */ @Test void shouldLoadSelectedObjectFactoryService() { Options options = () -> DefaultObjectFactory.class; @@ -40,37 +68,99 @@ void shouldLoadSelectedObjectFactoryService() { assertThat(loader.loadObjectFactory(), instanceOf(DefaultObjectFactory.class)); } + /** + * Test-case #4 + */ @Test - void shouldThrowIfDefaultObjectFactoryServiceCouldNotBeLoaded() { + void test_case_4() { + io.cucumber.core.backend.Options options = () -> null; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); + } + + /** + * Test-case #4 bis (reverse order) + */ + @Test + void test_case_4_with_services_in_reverse_order() { + io.cucumber.core.backend.Options options = () -> null; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + OtherFactory.class, + DefaultObjectFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); + } + + /** + * Test-case #5 + */ + @Test + void test_case_5() { + io.cucumber.core.backend.Options options = () -> DefaultObjectFactory.class; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(DefaultObjectFactory.class)); + } + + /** + * Test case #6 + */ + @Test + void test_case_6() { + // Given Options options = () -> null; - Supplier classLoader = () -> new FilteredClassLoader( - "META-INF/services/io.cucumber.core.backend.ObjectFactory"); ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( - classLoader, + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class, + YetAnotherFactory.class), options); + // When CucumberException exception = assertThrows(CucumberException.class, loader::loadObjectFactory); - assertThat(exception.getMessage(), is("" + - "Could not find any object factory.\n" + - "\n" + - "Cucumber uses SPI to discover object factory implementations.\n" + - "This typically happens when using shaded jars. Make sure\n" + - "to merge all SPI definitions in META-INF/services correctly")); + + // Then + assertThat(exception.getMessage(), + containsString("More than one Cucumber ObjectFactory was found on the classpath")); + } + + /** + * Test-case #7 + */ + @Test + void test_case_7() { + io.cucumber.core.backend.Options options = () -> OtherFactory.class; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + DefaultObjectFactory.class, + OtherFactory.class, + YetAnotherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); } + /** + * Test case #8 + */ @Test void shouldThrowIfSelectedObjectFactoryServiceCouldNotBeLoaded() { - Options options = () -> NoSuchObjectFactory.class; - Supplier classLoader = () -> new FilteredClassLoader( - "META-INF/services/io.cucumber.core.backend.ObjectFactory"); + Options options = () -> OtherFactory.class; ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( - classLoader, + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class), options); CucumberException exception = assertThrows(CucumberException.class, loader::loadObjectFactory); assertThat(exception.getMessage(), is("" + - "Could not find object factory io.cucumber.core.runtime.ObjectFactoryServiceLoaderTest$NoSuchObjectFactory.\n" + "Could not find object factory io.cucumber.core.runtime.ObjectFactoryServiceLoaderTest$OtherFactory.\n" + "\n" + "Cucumber uses SPI to discover object factory implementations.\n" + @@ -78,7 +168,20 @@ void shouldThrowIfSelectedObjectFactoryServiceCouldNotBeLoaded() { "the classpath?")); } - static class NoSuchObjectFactory implements ObjectFactory { + /** + * Test-case #9 + */ + @Test + void test_case_9() { + io.cucumber.core.backend.Options options = () -> null; + ObjectFactoryServiceLoader loader = new ObjectFactoryServiceLoader( + () -> new ServiceLoaderTestClassLoader(ObjectFactory.class, + OtherFactory.class), + options); + assertThat(loader.loadObjectFactory(), instanceOf(OtherFactory.class)); + } + + public static class FakeObjectFactory implements ObjectFactory { @Override public boolean addClass(Class glueClass) { @@ -102,25 +205,9 @@ public void stop() { } - private static class FilteredClassLoader extends URLClassLoader { - - private final Collection filteredResources; - - public FilteredClassLoader(String... filteredResources) { - super(new URL[0], FilteredClassLoader.class.getClassLoader()); - this.filteredResources = Arrays.asList(filteredResources); - } - - @Override - public Enumeration getResources(String name) throws IOException { - for (String filteredResource : filteredResources) { - if (name.equals(filteredResource)) { - return Collections.emptyEnumeration(); - } - } - return super.getResources(name); - } - + public static class OtherFactory extends FakeObjectFactory { } + public static class YetAnotherFactory extends FakeObjectFactory { + } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md new file mode 100644 index 0000000000..e5155b8b28 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md @@ -0,0 +1,18 @@ +# Testcases for `ObjectFactoryServiceLoader` + +| # | object-factory property | Available services | Result | +|---|-------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------| +| 1 | undefined | none | exception, no generators available | +| 2 | undefined | DefaultObjectFactory | DefaultObjectFactory used | +| 3 | DefaultObjectFactory | DefaultObjectFactory | DefaultObjectFactory used | +| 4 | undefined | DefaultObjectFactory, OtherFactory | OtherFactory used | +| 5 | DefaultObjectFactory | DefaultObjectFactory, OtherFactory | DefaultObjectFactory used | +| 6 | undefined | DefaultObjectFactory, OtherFactory, YetAnotherFactory | exception, cucumber couldn't decide multiple (non default) generators available | +| 7 | OtherFactory | DefaultObjectFactory, OtherFactory, YetAnotherFactory | OtherFactory used | +| 8 | OtherFactory | DefaultObjectFactory | exception, class not found through SPI | +| 9 | undefined | OtherFactory | OtherFactory used | + +Essentially this means that +* (2) Cucumber works by default +* (4) When adding a custom implementation to the class path it is used automatically +* When cucumber should not guess (5) or can not guess (7), the property is used to force a choice diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java new file mode 100644 index 0000000000..bfedc5a0d6 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ServiceLoaderTestClassLoader.java @@ -0,0 +1,96 @@ +package io.cucumber.core.runtime; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Collections; +import java.util.Enumeration; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Testing classloader for ServiceLoader. This classloader overrides the + * META-INF/services/ file with a custom definition. + */ +public class ServiceLoaderTestClassLoader extends URLClassLoader { + Class metaInfInterface; + Class[] implementingClasses; + + /** + * Constructs a classloader which has no + * META-INF/services/. + * + * @param metaInfInterface ServiceLoader interface + */ + public ServiceLoaderTestClassLoader(Class metaInfInterface) { + this(metaInfInterface, (Class[]) null); + } + + /** + * Constructs a fake META-INF/services/ file which + * contains the provided array of classes. When the implementingClasses + * array is null, the META-INF file will not be constructed. The classes + * from implementingClasses are not required to implement the + * metaInfInterface. + * + * @param metaInfInterface ServiceLoader interface + * @param implementingClasses potential subclasses of the ServiceLoader + * metaInfInterface + */ + public ServiceLoaderTestClassLoader(Class metaInfInterface, Class... implementingClasses) { + super(new URL[0], metaInfInterface.getClassLoader()); + if (!metaInfInterface.isInterface()) { + throw new IllegalArgumentException("the META-INF service " + metaInfInterface + " should be an interface"); + } + this.metaInfInterface = metaInfInterface; + this.implementingClasses = implementingClasses; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (name.equals("META-INF/services/" + metaInfInterface.getName())) { + if (implementingClasses == null) { + return Collections.emptyEnumeration(); + } + URL url = new URL("foo", "bar", 99, "/foobar", new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override + public void connect() { + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(Stream.of(implementingClasses) + .map(Class::getName) + .collect(Collectors.joining("\n")) + .getBytes()); + } + }; + } + }); + + return new Enumeration() { + boolean hasNext = true; + + @Override + public boolean hasMoreElements() { + return hasNext; + } + + @Override + public URL nextElement() { + hasNext = false; + return url; + } + }; + } + return super.getResources(name); + } + +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java new file mode 100644 index 0000000000..5843bbeedc --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java @@ -0,0 +1,268 @@ +package io.cucumber.core.runtime; + +import io.cucumber.core.eventbus.IncrementingUuidGenerator; +import io.cucumber.core.eventbus.Options; +import io.cucumber.core.eventbus.RandomUuidGenerator; +import io.cucumber.core.eventbus.UuidGenerator; +import io.cucumber.core.exception.CucumberException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.jupiter.api.Assertions.*; + +/** + * @see test-cases description + */ +class UuidGeneratorServiceLoaderTest { + + /** + * | 1 | undefined | none | exception, no generators available | + */ + @Test + void test_case_1() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class), + options); + + CucumberException exception = assertThrows(CucumberException.class, loader::loadUuidGenerator); + assertThat(exception.getMessage(), is("" + + "Could not find any UUID generator.\n" + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "This typically happens when using shaded jars. Make sure\n" + + "to merge all SPI definitions in META-INF/services correctly")); + } + + /** + * | 2 | undefined | RandomUuidGenerator, IncrementingUuidGenerator | + * RandomUuidGenerator used | + */ + @Test + void test_case_2() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + UuidGeneratorServiceLoaderTest.class::getClassLoader, + options); + assertThat(loader.loadUuidGenerator(), instanceOf(RandomUuidGenerator.class)); + } + + /** + * | 3 | RandomUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator | RandomUuidGenerator used | + */ + @Test + void test_case_3() { + Options options = () -> RandomUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + UuidGeneratorServiceLoaderTest.class::getClassLoader, + options); + assertThat(loader.loadUuidGenerator(), instanceOf(RandomUuidGenerator.class)); + } + + /** + * | 4 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, + * OtherGenerator | OtherGenerator used | + */ + @Test + void test_case_4() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 4bis | undefined | OtherGenerator, RandomUuidGenerator, + * IncrementingUuidGenerator | OtherGenerator used | + */ + @Test + void test_case_4_bis() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + OtherGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 5 | RandomUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator, OtherGenerator | RandomUuidGenerator used | + */ + @Test + void test_case_5() { + Options options = () -> RandomUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(RandomUuidGenerator.class)); + } + + /** + * | 6 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, + * OtherGenerator, YetAnotherGenerator | exception, cucumber couldn't decide + * multiple (non default) generators available | + */ + @Test + void test_case_6() { + // Given + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class, + YetAnotherGenerator.class), + options); + + // When + CucumberException cucumberException = assertThrows(CucumberException.class, loader::loadUuidGenerator); + + // Then + assertThat(cucumberException.getMessage(), + Matchers.containsString("More than one Cucumber UuidGenerator was found on the classpath")); + } + + /** + * | 7 | OtherGenerator | RandomUuidGenerator, IncrementingUuidGenerator, + * OtherGenerator, YetAnotherGenerator | OtherGenerator used | + */ + @Test + void test_case_7() { + Options options = () -> OtherGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class, + YetAnotherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 8 | IncrementingUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | + * IncrementingUuidGenerator used | + */ + @Test + void test_case_8() { + Options options = () -> IncrementingUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + RandomUuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class, + YetAnotherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(IncrementingUuidGenerator.class)); + } + + /** + * | 9 | IncrementingUuidGenerator | RandomUuidGenerator, + * IncrementingUuidGenerator | IncrementingUuidGenerator used | + */ + @Test + void test_case_9() { + Options options = () -> IncrementingUuidGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + UuidGeneratorServiceLoaderTest.class::getClassLoader, + options); + assertThat(loader.loadUuidGenerator(), instanceOf(IncrementingUuidGenerator.class)); + } + + /** + * | 10 | OtherGenerator | none | exception, generator OtherGenerator not + * available | + */ + @Test + void test_case_10() { + + Options options = () -> OtherGenerator.class; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class), + options); + + CucumberException exception = assertThrows(CucumberException.class, loader::loadUuidGenerator); + assertThat(exception.getMessage(), is("" + + "Could not find UUID generator io.cucumber.core.runtime.UuidGeneratorServiceLoaderTest$OtherGenerator.\n" + + + "\n" + + "Cucumber uses SPI to discover UUID generator implementations.\n" + + "Has the class been registered with SPI and is it available on\n" + + "the classpath?")); + } + + /** + * | 11 | undefined | OtherGenerator | OtherGenerator used | + */ + @Test + void test_case_11() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + OtherGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 12 | undefined | IncrementingUuidGenerator, OtherGenerator | + * OtherGenerator used | + */ + @Test + void test_case_12() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + IncrementingUuidGenerator.class, + OtherGenerator.class), + options); + + assertThat(loader.loadUuidGenerator(), instanceOf(OtherGenerator.class)); + } + + /** + * | 13 | undefined | IncrementingUuidGenerator | IncrementingUuidGenerator + * used | + */ + @Test + void test_case_13() { + Options options = () -> null; + UuidGeneratorServiceLoader loader = new UuidGeneratorServiceLoader( + () -> new ServiceLoaderTestClassLoader(UuidGenerator.class, + IncrementingUuidGenerator.class), + options); + assertThat(loader.loadUuidGenerator(), instanceOf(IncrementingUuidGenerator.class)); + } + + public static class OtherGenerator implements UuidGenerator { + @Override + public UUID get() { + return null; + } + } + + public static class YetAnotherGenerator implements UuidGenerator { + @Override + public UUID get() { + return null; + } + } + +} diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md new file mode 100644 index 0000000000..34cad9beb1 --- /dev/null +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md @@ -0,0 +1,17 @@ +# Testcases for `UuidGeneratorServiceLoader` + +| # | uuid-generator property | Available services | Result | +|-----|---------------------------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| 1 | undefined | none | exception, no generators available | +| 2 | undefined | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | +| 3 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | +| 4 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | +| 5 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | RandomUuidGenerator used | +| 6 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | exception, cucumber couldn't decide multiple (non default) generators available | +| 7 | OtherGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | OtherGenerator used | +| 8 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | IncrementingUuidGenerator used | +| 9 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | IncrementingUuidGenerator used | +| 10 | OtherGenerator | none | exception, generator OtherGenerator not available | +| 11 | undefined | OtherGenerator | OtherGenerator used | +| 12 | undefined | IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | +| 13 | undefined | IncrementingUuidGenerator | IncrementingUuidGenerator used | diff --git a/cucumber-deltaspike/pom.xml b/cucumber-deltaspike/pom.xml index 0885894bf7..148b653ee3 100644 --- a/cucumber-deltaspike/pom.xml +++ b/cucumber-deltaspike/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-deltaspike diff --git a/cucumber-gherkin-messages/pom.xml b/cucumber-gherkin-messages/pom.xml index d2f80cc903..1011cdc84f 100644 --- a/cucumber-gherkin-messages/pom.xml +++ b/cucumber-gherkin-messages/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT diff --git a/cucumber-gherkin/pom.xml b/cucumber-gherkin/pom.xml index 2e5f8b9b3c..bc428c332b 100644 --- a/cucumber-gherkin/pom.xml +++ b/cucumber-gherkin/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT diff --git a/cucumber-guice/pom.xml b/cucumber-guice/pom.xml index 9918cfbca1..ae605315c8 100644 --- a/cucumber-guice/pom.xml +++ b/cucumber-guice/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-guice diff --git a/cucumber-jakarta-cdi/pom.xml b/cucumber-jakarta-cdi/pom.xml index 7896b6ac27..103a00208a 100644 --- a/cucumber-jakarta-cdi/pom.xml +++ b/cucumber-jakarta-cdi/pom.xml @@ -16,7 +16,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-jakarta-cdi diff --git a/cucumber-jakarta-openejb/pom.xml b/cucumber-jakarta-openejb/pom.xml index e8ef2c342d..50e41f64a6 100644 --- a/cucumber-jakarta-openejb/pom.xml +++ b/cucumber-jakarta-openejb/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-jakarta-openejb diff --git a/cucumber-java/pom.xml b/cucumber-java/pom.xml index c91c4f70d7..f540d9a468 100644 --- a/cucumber-java/pom.xml +++ b/cucumber-java/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-java diff --git a/cucumber-java8/pom.xml b/cucumber-java8/pom.xml index f4e9d1eea4..d168bb549c 100644 --- a/cucumber-java8/pom.xml +++ b/cucumber-java8/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-java8 diff --git a/cucumber-junit-platform-engine/pom.xml b/cucumber-junit-platform-engine/pom.xml index 709748aa3e..27e6e0438a 100644 --- a/cucumber-junit-platform-engine/pom.xml +++ b/cucumber-junit-platform-engine/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-junit-platform-engine diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java index 233d6909a5..aed3d7a449 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java @@ -172,6 +172,16 @@ public final class Constants { */ public static final String OBJECT_FACTORY_PROPERTY_NAME = io.cucumber.core.options.Constants.OBJECT_FACTORY_PROPERTY_NAME; + /** + * Property name to select custom UUID generator implementation: {@value} + *

+ * By default, if a single UUID generator is available on the class path + * that object factory will be used, or more than one UUID generator and the + * #RandomUuidGenerator are available on the classpath, the + * #RandomUuidGenerator will be used. + */ + public static final String UUID_GENERATOR_PROPERTY_NAME = io.cucumber.core.options.Constants.UUID_GENERATOR_PROPERTY_NAME; + /** * Property name to control naming convention for generated snippets: * {@value} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java index c72a2af41d..09d7a40f8a 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java @@ -1,11 +1,13 @@ package io.cucumber.junit.platform.engine; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.feature.GluePath; import io.cucumber.core.options.ObjectFactoryParser; import io.cucumber.core.options.PluginOption; import io.cucumber.core.options.SnippetTypeParser; +import io.cucumber.core.options.UuidGeneratorParser; import io.cucumber.core.plugin.NoPublishFormatter; import io.cucumber.core.plugin.PublishFormatter; import io.cucumber.core.snippets.SnippetType; @@ -39,11 +41,13 @@ import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.SNIPPET_TYPE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.UUID_GENERATOR_PROPERTY_NAME; class CucumberEngineOptions implements io.cucumber.core.plugin.Options, io.cucumber.core.runner.Options, - io.cucumber.core.backend.Options { + io.cucumber.core.backend.Options, + io.cucumber.core.eventbus.Options { private final ConfigurationParameters configurationParameters; @@ -156,6 +160,13 @@ public Class getObjectFactoryClass() { .orElse(null); } + @Override + public Class getUuidGeneratorClass() { + return configurationParameters + .get(UUID_GENERATOR_PROPERTY_NAME, UuidGeneratorParser::parseUuidGenerator) + .orElse(null); + } + boolean isParallelExecutionEnabled() { return configurationParameters .getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME) diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java index 3f1bdaf915..e1260daf2d 100644 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java @@ -1,10 +1,11 @@ package io.cucumber.junit.platform.engine; +import io.cucumber.core.backend.DefaultObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import io.cucumber.core.plugin.Options; import io.cucumber.core.snippets.SnippetType; import org.junit.jupiter.api.Test; import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.support.hierarchical.Node; import java.net.URI; @@ -150,7 +151,25 @@ void isParallelExecutionEnabled() { ConfigurationParameters absent = new MapConfigurationParameters( "some key", "some value"); assertFalse(new CucumberEngineOptions(absent).isParallelExecutionEnabled()); + } + + @Test + void objectFactory() { + ConfigurationParameters configurationParameters = new MapConfigurationParameters( + Constants.OBJECT_FACTORY_PROPERTY_NAME, + DefaultObjectFactory.class.getName()); + assertThat(new CucumberEngineOptions(configurationParameters).getObjectFactoryClass(), + is(DefaultObjectFactory.class)); } + @Test + void uuidGenerator() { + ConfigurationParameters configurationParameters = new MapConfigurationParameters( + Constants.UUID_GENERATOR_PROPERTY_NAME, + IncrementingUuidGenerator.class.getName()); + + assertThat(new CucumberEngineOptions(configurationParameters).getUuidGeneratorClass(), + is(IncrementingUuidGenerator.class)); + } } diff --git a/cucumber-junit/pom.xml b/cucumber-junit/pom.xml index ad37bb09fc..8142b58a0c 100644 --- a/cucumber-junit/pom.xml +++ b/cucumber-junit/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-junit diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java b/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java index 455470cb1e..98a3094f30 100644 --- a/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java +++ b/cucumber-junit/src/main/java/io/cucumber/junit/CucumberOptions.java @@ -149,6 +149,17 @@ */ Class objectFactory() default NoObjectFactory.class; + /** + * Specify a custom ObjectFactory. + *

+ * In case a custom ObjectFactory is needed, the class can be specified + * here. A custom ObjectFactory might be needed when more granular control + * is needed over the dependency injection mechanism. + * + * @return an {@link io.cucumber.core.backend.ObjectFactory} implementation + */ + Class uuidGenerator() default NoUuidGenerator.class; + enum SnippetType { UNDERSCORE, CAMELCASE } diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java b/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java index ce7206026c..eafd59b962 100644 --- a/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java +++ b/cucumber-junit/src/main/java/io/cucumber/junit/JUnitCucumberOptionsProvider.java @@ -1,6 +1,7 @@ package io.cucumber.junit; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; import io.cucumber.core.options.CucumberOptionsAnnotationParser; @@ -102,6 +103,10 @@ public Class objectFactory() { return (annotation.objectFactory() == NoObjectFactory.class) ? null : annotation.objectFactory(); } + @Override + public Class uuidGenerator() { + return (annotation.uuidGenerator() == NoUuidGenerator.class) ? null : annotation.uuidGenerator(); + } } } diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java b/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java new file mode 100644 index 0000000000..91f40e3fd7 --- /dev/null +++ b/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java @@ -0,0 +1,20 @@ +package io.cucumber.junit; + +import io.cucumber.core.eventbus.UuidGenerator; + +import java.util.UUID; + +/** + * This UUID generator does nothing. It is solely needed for marking purposes. + */ +final class NoUuidGenerator implements UuidGenerator { + + private NoUuidGenerator() { + // No need for instantiation + } + + @Override + public UUID get() { + return null; + } +} diff --git a/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java b/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java index 43b84959fb..515f554ec2 100644 --- a/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java +++ b/cucumber-junit/src/test/java/io/cucumber/junit/JUnitCucumberOptionsProviderTest.java @@ -1,6 +1,7 @@ package io.cucumber.junit; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,6 +22,7 @@ void setUp() { void testObjectFactoryWhenNotSpecified() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithDefault.class); + assertNotNull(options); assertNull(options.objectFactory()); } @@ -28,10 +30,26 @@ void testObjectFactoryWhenNotSpecified() { void testObjectFactory() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithCustomObjectFactory.class); - assertNotNull(options.objectFactory()); + assertNotNull(options); assertEquals(TestObjectFactory.class, options.objectFactory()); } + @Test + void testUuidGeneratorWhenNotSpecified() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithDefault.class); + assertNotNull(options); + assertNull(options.uuidGenerator()); + } + + @Test + void testUuidGenerator() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithCustomUuidGenerator.class); + assertNotNull(options); + assertEquals(IncrementingUuidGenerator.class, options.uuidGenerator()); + } + @CucumberOptions() private static final class ClassWithDefault { @@ -42,6 +60,11 @@ private static final class ClassWithCustomObjectFactory { } + @CucumberOptions(uuidGenerator = IncrementingUuidGenerator.class) + private static final class ClassWithCustomUuidGenerator { + + } + private static final class TestObjectFactory implements ObjectFactory { @Override diff --git a/cucumber-kotlin-java8/pom.xml b/cucumber-kotlin-java8/pom.xml index b0e979f1d6..78575066df 100644 --- a/cucumber-kotlin-java8/pom.xml +++ b/cucumber-kotlin-java8/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-kotlin-java8 diff --git a/cucumber-openejb/pom.xml b/cucumber-openejb/pom.xml index 3935e8a197..b4078bc381 100644 --- a/cucumber-openejb/pom.xml +++ b/cucumber-openejb/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-openejb diff --git a/cucumber-picocontainer/pom.xml b/cucumber-picocontainer/pom.xml index 4ccc244368..269332be47 100644 --- a/cucumber-picocontainer/pom.xml +++ b/cucumber-picocontainer/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-picocontainer diff --git a/cucumber-plugin/pom.xml b/cucumber-plugin/pom.xml index f573fdeea9..1f58288b92 100644 --- a/cucumber-plugin/pom.xml +++ b/cucumber-plugin/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-plugin diff --git a/cucumber-spring/pom.xml b/cucumber-spring/pom.xml index 8292b57ca5..028f92ae15 100644 --- a/cucumber-spring/pom.xml +++ b/cucumber-spring/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-spring diff --git a/cucumber-testng/pom.xml b/cucumber-testng/pom.xml index a023bcbc9d..00d83fa394 100644 --- a/cucumber-testng/pom.xml +++ b/cucumber-testng/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT cucumber-testng diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java b/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java index 84d6abd366..9b7f9fbbea 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java @@ -122,6 +122,17 @@ */ Class objectFactory() default NoObjectFactory.class; + /** + * Specify a custom ObjectFactory. + *

+ * In case a custom ObjectFactory is needed, the class can be specified + * here. A custom ObjectFactory might be needed when more granular control + * is needed over the dependency injection mechanism. + * + * @return an {@link io.cucumber.core.backend.ObjectFactory} implementation + */ + Class uuidGenerator() default NoUuidGenerator.class; + enum SnippetType { UNDERSCORE, CAMELCASE } diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java b/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java new file mode 100644 index 0000000000..25a4b6759b --- /dev/null +++ b/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java @@ -0,0 +1,20 @@ +package io.cucumber.testng; + +import io.cucumber.core.eventbus.UuidGenerator; + +import java.util.UUID; + +/** + * This UUID generator does nothing. It is solely needed for marking purposes. + */ +final class NoUuidGenerator implements UuidGenerator { + + private NoUuidGenerator() { + // No need for instantiation + } + + @Override + public UUID get() { + return null; + } +} diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java b/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java index ed8218f537..52d1b81a1a 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java @@ -1,6 +1,7 @@ package io.cucumber.testng; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; import io.cucumber.core.options.CucumberOptionsAnnotationParser; @@ -102,6 +103,10 @@ public Class objectFactory() { return (annotation.objectFactory() == NoObjectFactory.class) ? null : annotation.objectFactory(); } + @Override + public Class uuidGenerator() { + return (annotation.uuidGenerator() == NoUuidGenerator.class) ? null : annotation.uuidGenerator(); + } } } diff --git a/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java b/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java index c7b4ccc8c2..40045bd841 100644 --- a/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java +++ b/cucumber-testng/src/test/java/io/cucumber/testng/TestNGCucumberOptionsProviderTest.java @@ -1,6 +1,7 @@ package io.cucumber.testng; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -21,6 +22,7 @@ void setUp() { void testObjectFactoryWhenNotSpecified() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithDefault.class); + assertNotNull(options); assertNull(options.objectFactory()); } @@ -28,10 +30,26 @@ void testObjectFactoryWhenNotSpecified() { void testObjectFactory() { io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider .getOptions(ClassWithCustomObjectFactory.class); - assertNotNull(options.objectFactory()); + assertNotNull(options); assertEquals(TestObjectFactory.class, options.objectFactory()); } + @Test + void testUuidGeneratorWhenNotSpecified() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithDefault.class); + assertNotNull(options); + assertNull(options.uuidGenerator()); + } + + @Test + void testUuidGenerator() { + io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions options = this.optionsProvider + .getOptions(ClassWithCustomUuidGenerator.class); + assertNotNull(options); + assertEquals(IncrementingUuidGenerator.class, options.uuidGenerator()); + } + @CucumberOptions() private static final class ClassWithDefault { @@ -42,6 +60,11 @@ private static final class ClassWithCustomObjectFactory { } + @CucumberOptions(uuidGenerator = IncrementingUuidGenerator.class) + private static final class ClassWithCustomUuidGenerator { + + } + private static final class TestObjectFactory implements ObjectFactory { @Override diff --git a/datatable-matchers/pom.xml b/datatable-matchers/pom.xml index 659b66be80..1aedc0c903 100644 --- a/datatable-matchers/pom.xml +++ b/datatable-matchers/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT datatable-matchers diff --git a/datatable/pom.xml b/datatable/pom.xml index 315a1fefc0..dceb876af8 100644 --- a/datatable/pom.xml +++ b/datatable/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT datatable diff --git a/docstring/pom.xml b/docstring/pom.xml index 26c12cb1cc..7606517d44 100644 --- a/docstring/pom.xml +++ b/docstring/pom.xml @@ -3,7 +3,7 @@ cucumber-jvm io.cucumber - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT 4.0.0 diff --git a/examples/calculator-java-cli/pom.xml b/examples/calculator-java-cli/pom.xml index 0bba7e28e1..82c51bd6e5 100644 --- a/examples/calculator-java-cli/pom.xml +++ b/examples/calculator-java-cli/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-cli diff --git a/examples/calculator-java-junit4/pom.xml b/examples/calculator-java-junit4/pom.xml index dff7ea4334..0adaf774d2 100644 --- a/examples/calculator-java-junit4/pom.xml +++ b/examples/calculator-java-junit4/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-junit4 diff --git a/examples/calculator-java-junit5/pom.xml b/examples/calculator-java-junit5/pom.xml index d70df8cbd5..ca450299e3 100644 --- a/examples/calculator-java-junit5/pom.xml +++ b/examples/calculator-java-junit5/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-junit5 diff --git a/examples/calculator-java-testng/pom.xml b/examples/calculator-java-testng/pom.xml index 768e375ffc..46d5f22a6b 100644 --- a/examples/calculator-java-testng/pom.xml +++ b/examples/calculator-java-testng/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java-testng diff --git a/examples/calculator-java8-cli/pom.xml b/examples/calculator-java8-cli/pom.xml index e941f496ac..c6c8bb3df3 100644 --- a/examples/calculator-java8-cli/pom.xml +++ b/examples/calculator-java8-cli/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT calculator-java8-cli diff --git a/examples/pom.xml b/examples/pom.xml index 0e16a909a6..08f2bed077 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT examples diff --git a/examples/spring-java-junit5/pom.xml b/examples/spring-java-junit5/pom.xml index 21026bdc1c..f9b23501c7 100644 --- a/examples/spring-java-junit5/pom.xml +++ b/examples/spring-java-junit5/pom.xml @@ -4,7 +4,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT spring-java-junit5 diff --git a/examples/wicket-java-junit4/pom.xml b/examples/wicket-java-junit4/pom.xml index bd2b005cca..b00a8c9311 100644 --- a/examples/wicket-java-junit4/pom.xml +++ b/examples/wicket-java-junit4/pom.xml @@ -3,7 +3,7 @@ io.cucumber examples - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT wicket-java-junit4 pom diff --git a/examples/wicket-java-junit4/wicket-main/pom.xml b/examples/wicket-java-junit4/wicket-main/pom.xml index c072b47901..e2c331d77c 100644 --- a/examples/wicket-java-junit4/wicket-main/pom.xml +++ b/examples/wicket-java-junit4/wicket-main/pom.xml @@ -3,7 +3,7 @@ io.cucumber wicket-java-junit4 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT wicket-main Examples: Wicket application diff --git a/examples/wicket-java-junit4/wicket-test/pom.xml b/examples/wicket-java-junit4/wicket-test/pom.xml index bd2aa7ce1f..5e42ba5ac0 100644 --- a/examples/wicket-java-junit4/wicket-test/pom.xml +++ b/examples/wicket-java-junit4/wicket-test/pom.xml @@ -3,7 +3,7 @@ io.cucumber wicket-java-junit4 - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT wicket-test Examples: Wicket application tested with Selenium diff --git a/pom.xml b/pom.xml index f83116a34a..03722369e5 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ cucumber-jvm - 7.11.2-SNAPSHOT + 7.12.0-SNAPSHOT pom Cucumber-JVM Cucumber for the JVM @@ -18,7 +18,7 @@ 1.8 8 - 1674814830 + 1678784521 scm:git:git://github.com/cucumber/cucumber-jvm.git From 6812c0f9501e327093bb1fdabd69ae37980c56ba Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Tue, 28 Mar 2023 10:43:30 +0200 Subject: [PATCH 3/8] feat: improved UuidGenerator API, IncrementingUuidGenerator generator, corrected Java8 compilation issue --- .../eventbus/IncrementingUuidGenerator.java | 81 ++++++++- .../core/eventbus/RandomUuidGenerator.java | 2 +- .../cucumber/core/eventbus/UuidGenerator.java | 7 +- .../IncrementingUuidGeneratorTest.java | 171 +++++++++++++++--- .../eventbus/RandomUuidGeneratorTest.java | 4 +- .../options/CommandlineOptionsParserTest.java | 2 +- .../core/options/NoUuidGenerator.java | 2 +- .../UuidGeneratorServiceLoaderTest.java | 4 +- .../io/cucumber/junit/NoUuidGenerator.java | 2 +- .../io/cucumber/testng/NoUuidGenerator.java | 2 +- 10 files changed, 234 insertions(+), 43 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java index 35fa782e79..79dc2eee83 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java @@ -2,6 +2,7 @@ import io.cucumber.core.exception.CucumberException; +import java.util.Random; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; @@ -11,30 +12,90 @@ * about 100 times faster than #RandomUuidGenerator. If you use Cucumber in * multi-JVM setup, you should use #RandomUuidGenerator instead. Note that the * UUID version and variant is not guaranteed to be stable. + * + * Properties: + * - thread-safe + * - collision-free in the same classloader + * - almost collision-free in different classloaders / JVMs + * - UUIDs generator using a given instance are sortable + * + * UUID version 8 (custom) ... + * + * | 40 bits | 8 bits | 4 bits | 12 bits | 2 bits | 62 bits | + * | -------------------| -------------- | ------- | ------------- | ------- | ------- | + * | LSBs of epoch-time | sessionCounter | version | classloaderId | variant | counter | + * */ public class IncrementingUuidGenerator implements UuidGenerator { - private static final AtomicLong sessionCounter = new AtomicLong(Long.MIN_VALUE); + /** + * 40 bits mask for the epoch-time part (MSB). + */ + public static final long MAX_EPOCH_TIME = 0x0ffffffffffL; - private final long sessionId; - private final AtomicLong counter = new AtomicLong(Long.MIN_VALUE); + /** + * 8 bits mask for the session identifier (MSB). Package-private for testing + * purposes. + */ + static final long MAX_SESSION_ID = 0xffL; + + /** + * 62 bits mask for the counter value (LSB) + */ + static final long MAX_COUNTER_VALUE = 0x3fffffffffffffffL; + + /** + * Classloader identifier (MSB). The identifier is a pseudo-random number on + * 12 bits. Note: there is no need to save the Random because it's static. + */ + @SuppressWarnings("java:S2119") + static final long CLASSLOADER_ID = new Random().nextInt() & 0x0fff; + + /** + * Session counter to differentiate instances created within a given + * classloader (MSB). + */ + static final AtomicLong sessionCounter = new AtomicLong(-1); + + /** + * Computed UUID MSB value. + */ + final long msb; + + /** + * Counter for the UUID LSB. + */ + final AtomicLong counter = new AtomicLong(-1); public IncrementingUuidGenerator() { - sessionId = sessionCounter.incrementAndGet(); + long sessionId = sessionCounter.incrementAndGet(); + if (sessionId == MAX_SESSION_ID) { + throw new CucumberException( + "Out of " + IncrementingUuidGenerator.class.getSimpleName() + + " capacity. Please reuse existing instances or use another " + + UuidGenerator.class.getSimpleName() + " implementation instead."); + } + long epochTime = System.currentTimeMillis(); + // msb = epochTime | sessionId | version | classloaderId + msb = ((epochTime & MAX_EPOCH_TIME) << 24) | (sessionId << 16) | (8 << 12) | CLASSLOADER_ID; } /** * Generate a new UUID. Will throw an exception when out of capacity. - * + * * @return a non-null UUID * @throws CucumberException when out of capacity */ @Override - public UUID get() { - long leastSigBits = counter.incrementAndGet(); - if (leastSigBits == Long.MAX_VALUE) { + public UUID generateId() { + long counterValue = counter.incrementAndGet(); + if (counterValue == MAX_COUNTER_VALUE) { throw new CucumberException( - "Out of IncrementingUuidGenerator capacity. Please use the RandomUuidGenerator instead."); + "Out of " + IncrementingUuidGenerator.class.getSimpleName() + + " capacity. Please generate using a new instance or use another " + + UuidGenerator.class.getSimpleName() + "implementation."); } - return new UUID(sessionId, leastSigBits); + long leastSigBits = counterValue | 0x8000000000000000L; // set variant + return new UUID(msb, leastSigBits); } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java index 6ecdf84ac2..76f34deb39 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/RandomUuidGenerator.java @@ -8,7 +8,7 @@ */ public class RandomUuidGenerator implements UuidGenerator { @Override - public UUID get() { + public UUID generateId() { return UUID.randomUUID(); } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java index 6557576ee4..b698bbf96b 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/UuidGenerator.java @@ -8,6 +8,11 @@ /** * SPI (Service Provider Interface) to generate UUIDs. */ -@API(status = API.Status.STABLE) +@API(status = API.Status.EXPERIMENTAL) public interface UuidGenerator extends Supplier { + UUID generateId(); + + default UUID get() { + return generateId(); + } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java index a3fab341f1..02d66acec9 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java @@ -4,40 +4,109 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import java.lang.reflect.Field; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class IncrementingUuidGeneratorTest { + + /** + * Example of generated values (same epochTime, same sessionId, same + * classloaderId, different counter value): + * "87273d64-5500-83e3-8000-000000000000" + * "87273d64-5500-83e3-8000-000000000001" + * "87273d64-5500-83e3-8000-000000000002" + * "87273d64-5500-83e3-8000-000000000003" + * "87273d64-5500-83e3-8000-000000000004" + * "87273d64-5500-83e3-8000-000000000005" + * "87273d64-5500-83e3-8000-000000000006" + * "87273d64-5500-83e3-8000-000000000007" + * "87273d64-5500-83e3-8000-000000000008" + * "87273d64-5500-83e3-8000-000000000009" + */ @Test void generates_different_non_null_uuids() { // Given UuidGenerator generator = new IncrementingUuidGenerator(); - UUID uuid1 = generator.get(); // When - UUID uuid2 = generator.get(); + List uuids = IntStream.rangeClosed(1, 10) + .mapToObj(i -> generator.generateId()) + .collect(Collectors.toList()); + + // Then + checkUuidProperties(uuids); + } + + /** + * Example of generated values (same epochTime, different sessionId, * same + * classloaderId, same counter value): + * "87273c5d-8500-88b6-8000-000000000000" + * "87273c5d-8501-88b6-8000-000000000000" + * "87273c5d-8502-88b6-8000-000000000000" + * "87273c5d-8503-88b6-8000-000000000000" + * "87273c5d-8504-88b6-8000-000000000000" + * "87273c5d-8505-88b6-8000-000000000000" + * "87273c5d-8506-88b6-8000-000000000000" + * "87273c5d-8507-88b6-8000-000000000000" + * "87273c5d-8508-88b6-8000-000000000000" + * "87273c5d-8509-88b6-8000-000000000000" + */ + @Test + void same_thread_generates_different_UuidGenerators() { + // Given/When + List uuids = IntStream.rangeClosed(1, 10) + .mapToObj(i -> new IncrementingUuidGenerator().generateId()) + .collect(Collectors.toList()); // Then - assertNotNull(uuid1); - assertNotNull(uuid2); - assertNotEquals(uuid1, uuid2); + checkUuidProperties(uuids); } + /** + * Example of values generated using different classloaders (same epochTime, + * same sessionId, different classloaderId, same counter value): + * "87273a9d-9a00-8bf7-8000-000000000000" + * "87273a9d-9c00-844e-8000-000000000000" + * "87273a9d-9e00-89ad-8000-000000000000" + * "87273a9d-a000-8fd9-8000-000000000000" + * "87273a9d-a100-8a48-8000-000000000000" + * "87273a9d-a400-8322-8000-000000000000" + * "87273a9d-a600-872c-8000-000000000000" + * "87273a9d-a700-88c9-8000-000000000000" + * "87273a9d-a900-8eb4-8000-000000000000" + * "87273a9d-ab00-898c-8000-000000000000" + */ @Test - void raises_exception_when_out_of_range() throws NoSuchFieldException, IllegalAccessException { + void different_classloaders_generators() { + // Given/When + List uuids = IntStream.rangeClosed(1, 10) + .mapToObj(i -> getUuidGeneratorFromOtherClassloader().generateId()) + .collect(Collectors.toList()); + + // Then + checkUuidProperties(uuids); + } + + @Test + void raises_exception_when_out_of_range() { // Given - UuidGenerator generator = new IncrementingUuidGenerator(); - Field counterField = IncrementingUuidGenerator.class.getDeclaredField("counter"); - counterField.setAccessible(true); - AtomicLong counter = (AtomicLong) counterField.get(generator); - counter.set(Long.MAX_VALUE - 1); + IncrementingUuidGenerator generator = new IncrementingUuidGenerator(); + generator.counter.set(IncrementingUuidGenerator.MAX_COUNTER_VALUE - 1); // When - CucumberException cucumberException = assertThrows(CucumberException.class, generator::get); + CucumberException cucumberException = assertThrows(CucumberException.class, generator::generateId); // Then assertThat(cucumberException.getMessage(), @@ -45,18 +114,74 @@ void raises_exception_when_out_of_range() throws NoSuchFieldException, IllegalAc } @Test - void same_thread_generates_different_UuidGenerators() { + void version_overflow() { // Given - UuidGenerator generator1 = new IncrementingUuidGenerator(); - UuidGenerator generator2 = new IncrementingUuidGenerator(); + IncrementingUuidGenerator.sessionCounter.set(IncrementingUuidGenerator.MAX_SESSION_ID - 1); // When - UUID uuid1 = generator1.get(); - UUID uuid2 = generator2.get(); + CucumberException cucumberException = assertThrows(CucumberException.class, IncrementingUuidGenerator::new); // Then - assertNotNull(uuid1); - assertNotNull(uuid2); - assertNotEquals(uuid1, uuid2); + assertThat(cucumberException.getMessage(), + Matchers.containsString("Out of IncrementingUuidGenerator capacity")); + } + + private static void checkUuidProperties(List uuids) { + // all UUIDs are non-null + assertTrue(uuids.stream().filter(Objects::isNull).findFirst().isEmpty()); + + // UUID version is always 8 + List versions = uuids.stream().map(UUID::version).distinct().collect(Collectors.toList()); + assertEquals(1, versions.size()); + assertEquals(8, versions.get(0)); + + // UUID variants is always 2 + List variants = uuids.stream().map(UUID::variant).distinct().collect(Collectors.toList()); + assertEquals(1, variants.size()); + assertEquals(2, variants.get(0)); + + // all UUIDs are distinct + assertEquals(uuids.size(), uuids.stream().distinct().count()); + + // all UUIDs are ordered + assertEquals(uuids, uuids.stream().sorted().collect(Collectors.toList())); + } + + private static UuidGenerator getUuidGeneratorFromOtherClassloader() { + try { + return (UuidGenerator) (new NonCachingClassLoader() + .findClass(IncrementingUuidGenerator.class.getName()) + .getConstructor() + .newInstance()); + } catch (Exception e) { + throw new RuntimeException("could not instantiate " + IncrementingUuidGenerator.class.getSimpleName(), e); + } + } + + /** + * A classloader which does not cache the class definition. Thus, when the + * Class loaded using #findClass will have different static fields. + */ + private static class NonCachingClassLoader extends ClassLoader { + + public NonCachingClassLoader() { + } + + @Override + protected Class findClass(String name) { + byte[] classBytes = loadClassBytesFromDisk(name); + return defineClass(name, classBytes, 0, classBytes.length); + } + + private byte[] loadClassBytesFromDisk(String className) { + try { + return Files.readAllBytes(Path.of(Objects.requireNonNull(NonCachingClassLoader.class + .getResource(className.replaceFirst(".+\\.", "") + ".class")).toURI())); + } catch (IOException e) { + throw new RuntimeException("Unable to read file from disk"); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java index c578c04d1e..7fd7ba800d 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/RandomUuidGeneratorTest.java @@ -11,10 +11,10 @@ class RandomUuidGeneratorTest { void generates_different_non_null_uuids() { // Given UuidGenerator generator = new RandomUuidGenerator(); - UUID uuid1 = generator.get(); + UUID uuid1 = generator.generateId(); // When - UUID uuid2 = generator.get(); + UUID uuid2 = generator.generateId(); // Then assertNotNull(uuid1); diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java index 5d31229ae3..3fc5a35a42 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java @@ -95,7 +95,7 @@ void has_version_from_properties_file() { } private String output() { - return out.toString(StandardCharsets.UTF_8); + return new String(out.toByteArray(), StandardCharsets.UTF_8); } @Test diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java b/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java index a8ddb653b0..73ea1988e3 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/NoUuidGenerator.java @@ -14,7 +14,7 @@ private NoUuidGenerator() { } @Override - public UUID get() { + public UUID generateId() { return null; } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java index 5843bbeedc..9ac17bbb4d 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java @@ -253,14 +253,14 @@ void test_case_13() { public static class OtherGenerator implements UuidGenerator { @Override - public UUID get() { + public UUID generateId() { return null; } } public static class YetAnotherGenerator implements UuidGenerator { @Override - public UUID get() { + public UUID generateId() { return null; } } diff --git a/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java b/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java index 91f40e3fd7..8e3237499f 100644 --- a/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java +++ b/cucumber-junit/src/main/java/io/cucumber/junit/NoUuidGenerator.java @@ -14,7 +14,7 @@ private NoUuidGenerator() { } @Override - public UUID get() { + public UUID generateId() { return null; } } diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java b/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java index 25a4b6759b..0ab6353afd 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/NoUuidGenerator.java @@ -14,7 +14,7 @@ private NoUuidGenerator() { } @Override - public UUID get() { + public UUID generateId() { return null; } } From 4d4ef8f70c3d9ee32cef1794e4d61555448945f3 Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Tue, 28 Mar 2023 11:12:13 +0200 Subject: [PATCH 4/8] feat: moved doc to test classes --- .../ObjectFactoryServiceLoaderTest.java | 21 ++++++++++++++++++- .../runtime/ObjectFactoryServiceLoaderTest.md | 18 ---------------- .../UuidGeneratorServiceLoaderTest.java | 20 +++++++++++++++++- .../runtime/UuidGeneratorServiceLoaderTest.md | 17 --------------- 4 files changed, 39 insertions(+), 37 deletions(-) delete mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md delete mode 100644 cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java index 14580387cd..6b717d9b5f 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.java @@ -20,7 +20,26 @@ import static org.junit.jupiter.api.Assertions.assertThrows; /** - * @see test-cases description + * Testcases for `ObjectFactoryServiceLoader` + * + * + * | # | object-factory property | Available services | Result | + * |---|-------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------| + * | 1 | undefined | none | exception, no generators available | + * | 2 | undefined | DefaultObjectFactory | DefaultObjectFactory used | + * | 3 | DefaultObjectFactory | DefaultObjectFactory | DefaultObjectFactory used | + * | 4 | undefined | DefaultObjectFactory, OtherFactory | OtherFactory used | + * | 5 | DefaultObjectFactory | DefaultObjectFactory, OtherFactory | DefaultObjectFactory used | + * | 6 | undefined | DefaultObjectFactory, OtherFactory, YetAnotherFactory | exception, cucumber couldn't decide multiple (non default) generators available | + * | 7 | OtherFactory | DefaultObjectFactory, OtherFactory, YetAnotherFactory | OtherFactory used | + * | 8 | OtherFactory | DefaultObjectFactory | exception, class not found through SPI | + * | 9 | undefined | OtherFactory | OtherFactory used | + * + * + * Essentially this means that + * * (2) Cucumber works by default + * * (4) When adding a custom implementation to the class path it is used automatically + * * When cucumber should not guess (5) or can not guess (7), the property is used to force a choice */ class ObjectFactoryServiceLoaderTest { diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md b/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md deleted file mode 100644 index e5155b8b28..0000000000 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/ObjectFactoryServiceLoaderTest.md +++ /dev/null @@ -1,18 +0,0 @@ -# Testcases for `ObjectFactoryServiceLoader` - -| # | object-factory property | Available services | Result | -|---|-------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------| -| 1 | undefined | none | exception, no generators available | -| 2 | undefined | DefaultObjectFactory | DefaultObjectFactory used | -| 3 | DefaultObjectFactory | DefaultObjectFactory | DefaultObjectFactory used | -| 4 | undefined | DefaultObjectFactory, OtherFactory | OtherFactory used | -| 5 | DefaultObjectFactory | DefaultObjectFactory, OtherFactory | DefaultObjectFactory used | -| 6 | undefined | DefaultObjectFactory, OtherFactory, YetAnotherFactory | exception, cucumber couldn't decide multiple (non default) generators available | -| 7 | OtherFactory | DefaultObjectFactory, OtherFactory, YetAnotherFactory | OtherFactory used | -| 8 | OtherFactory | DefaultObjectFactory | exception, class not found through SPI | -| 9 | undefined | OtherFactory | OtherFactory used | - -Essentially this means that -* (2) Cucumber works by default -* (4) When adding a custom implementation to the class path it is used automatically -* When cucumber should not guess (5) or can not guess (7), the property is used to force a choice diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java index 9ac17bbb4d..8cdf252fea 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.java @@ -16,7 +16,25 @@ import static org.junit.jupiter.api.Assertions.*; /** - * @see test-cases description + * # Testcases for `UuidGeneratorServiceLoader` + * + * + * | # | uuid-generator property | Available services | Result | + * |-----|---------------------------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| + * | 1 | undefined | none | exception, no generators available | + * | 2 | undefined | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | + * | 3 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | + * | 4 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | + * | 5 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | RandomUuidGenerator used | + * | 6 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | exception, cucumber couldn't decide multiple (non default) generators available | + * | 7 | OtherGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | OtherGenerator used | + * | 8 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | IncrementingUuidGenerator used | + * | 9 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | IncrementingUuidGenerator used | + * | 10 | OtherGenerator | none | exception, generator OtherGenerator not available | + * | 11 | undefined | OtherGenerator | OtherGenerator used | + * | 12 | undefined | IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | + * | 13 | undefined | IncrementingUuidGenerator | IncrementingUuidGenerator used | + * */ class UuidGeneratorServiceLoaderTest { diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md b/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md deleted file mode 100644 index 34cad9beb1..0000000000 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/UuidGeneratorServiceLoaderTest.md +++ /dev/null @@ -1,17 +0,0 @@ -# Testcases for `UuidGeneratorServiceLoader` - -| # | uuid-generator property | Available services | Result | -|-----|---------------------------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| -| 1 | undefined | none | exception, no generators available | -| 2 | undefined | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | -| 3 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | RandomUuidGenerator used | -| 4 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | -| 5 | RandomUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator | RandomUuidGenerator used | -| 6 | undefined | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | exception, cucumber couldn't decide multiple (non default) generators available | -| 7 | OtherGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | OtherGenerator used | -| 8 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator, OtherGenerator, YetAnotherGenerator | IncrementingUuidGenerator used | -| 9 | IncrementingUuidGenerator | RandomUuidGenerator, IncrementingUuidGenerator | IncrementingUuidGenerator used | -| 10 | OtherGenerator | none | exception, generator OtherGenerator not available | -| 11 | undefined | OtherGenerator | OtherGenerator used | -| 12 | undefined | IncrementingUuidGenerator, OtherGenerator | OtherGenerator used | -| 13 | undefined | IncrementingUuidGenerator | IncrementingUuidGenerator used | From 2eebb5e29c50e7a7161415ecb46a58778a63844f Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Tue, 28 Mar 2023 11:31:36 +0200 Subject: [PATCH 5/8] feat: corrected testcase for unix build --- .../core/eventbus/IncrementingUuidGenerator.java | 2 +- .../eventbus/IncrementingUuidGeneratorTest.java | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java index 79dc2eee83..cfa7758204 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java @@ -17,7 +17,7 @@ * - thread-safe * - collision-free in the same classloader * - almost collision-free in different classloaders / JVMs - * - UUIDs generator using a given instance are sortable + * - UUIDs generated using the instances from the same classloader are sortable * * UUID version 8 (custom) ... diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java index 02d66acec9..08bcac9ae9 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java @@ -144,7 +144,20 @@ private static void checkUuidProperties(List uuids) { assertEquals(uuids.size(), uuids.stream().distinct().count()); // all UUIDs are ordered - assertEquals(uuids, uuids.stream().sorted().collect(Collectors.toList())); + assertEquals(uuids.stream() + .map(IncrementingUuidGeneratorTest::removeClassloaderId) + .collect(Collectors.toList()), + uuids.stream() + .map(IncrementingUuidGeneratorTest::removeClassloaderId) + .sorted() + .collect(Collectors.toList())); + } + + /** + * Create a copy of the UUID without the random part to allow comparison. + */ + private static UUID removeClassloaderId(UUID uuid) { + return new UUID(uuid.getMostSignificantBits() & 0xfffffffffffff000L, uuid.getLeastSignificantBits()); } private static UuidGenerator getUuidGeneratorFromOtherClassloader() { From e1f3fdd5f6761d778448163f349bca4db434bf5b Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Tue, 28 Mar 2023 14:25:47 +0200 Subject: [PATCH 6/8] feat: corrected testcase for unix build --- .../core/eventbus/IncrementingUuidGeneratorTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java index 08bcac9ae9..d36d706245 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/eventbus/IncrementingUuidGeneratorTest.java @@ -145,9 +145,9 @@ private static void checkUuidProperties(List uuids) { // all UUIDs are ordered assertEquals(uuids.stream() - .map(IncrementingUuidGeneratorTest::removeClassloaderId) - .collect(Collectors.toList()), - uuids.stream() + .map(IncrementingUuidGeneratorTest::removeClassloaderId) + .collect(Collectors.toList()), + uuids.stream() .map(IncrementingUuidGeneratorTest::removeClassloaderId) .sorted() .collect(Collectors.toList())); From 9fad222d20bbd2e16c471563822ab6eb3eb8d1b3 Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Wed, 29 Mar 2023 12:00:40 +0200 Subject: [PATCH 7/8] feat: corrected conversion for java8 build --- .../io/cucumber/core/options/CommandlineOptionsParserTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java index 5d31229ae3..3fc5a35a42 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CommandlineOptionsParserTest.java @@ -95,7 +95,7 @@ void has_version_from_properties_file() { } private String output() { - return out.toString(StandardCharsets.UTF_8); + return new String(out.toByteArray(), StandardCharsets.UTF_8); } @Test From 4c4c6a07d97e931da7e5e149a2423e354830b1f5 Mon Sep 17 00:00:00 2001 From: Julien Kronegg Date: Thu, 6 Apr 2023 07:04:26 +0200 Subject: [PATCH 8/8] fix: cleanup javadoc and made field private --- .../cucumber/core/eventbus/IncrementingUuidGenerator.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java index cfa7758204..e612bfcfd6 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java +++ b/cucumber-core/src/main/java/io/cucumber/core/eventbus/IncrementingUuidGenerator.java @@ -9,9 +9,7 @@ /** * 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. If you use Cucumber in - * multi-JVM setup, you should use #RandomUuidGenerator instead. Note that the - * UUID version and variant is not guaranteed to be stable. + * about 100 times faster than #RandomUuidGenerator. * * Properties: * - thread-safe @@ -19,7 +17,7 @@ * - almost collision-free in different classloaders / JVMs * - UUIDs generated using the instances from the same classloader are sortable * - * UUID version 8 (custom) ... * * | 40 bits | 8 bits | 4 bits | 12 bits | 2 bits | 62 bits | @@ -31,7 +29,7 @@ public class IncrementingUuidGenerator implements UuidGenerator { /** * 40 bits mask for the epoch-time part (MSB). */ - public static final long MAX_EPOCH_TIME = 0x0ffffffffffL; + private static final long MAX_EPOCH_TIME = 0x0ffffffffffL; /** * 8 bits mask for the session identifier (MSB). Package-private for testing