diff --git a/junit/src/main/java/cucumber/runtime/junit/ExamplesRunner.java b/junit/src/main/java/cucumber/runtime/junit/ExamplesRunner.java index d548222da0..398064506f 100644 --- a/junit/src/main/java/cucumber/runtime/junit/ExamplesRunner.java +++ b/junit/src/main/java/cucumber/runtime/junit/ExamplesRunner.java @@ -14,21 +14,23 @@ public class ExamplesRunner extends Suite { private final CucumberExamples cucumberExamples; + private final IdProvider idProvider; private Description description; private JUnitReporter jUnitReporter; - protected ExamplesRunner(Runtime runtime, CucumberExamples cucumberExamples, JUnitReporter jUnitReporter) throws InitializationError { - super(ExamplesRunner.class, buildRunners(runtime, cucumberExamples, jUnitReporter)); + protected ExamplesRunner(Runtime runtime, CucumberExamples cucumberExamples, JUnitReporter jUnitReporter, IdProvider idProvider) throws InitializationError { + super(ExamplesRunner.class, buildRunners(runtime, cucumberExamples, jUnitReporter, idProvider)); this.cucumberExamples = cucumberExamples; this.jUnitReporter = jUnitReporter; + this.idProvider = idProvider; } - private static List buildRunners(Runtime runtime, CucumberExamples cucumberExamples, JUnitReporter jUnitReporter) { + private static List buildRunners(Runtime runtime, CucumberExamples cucumberExamples, JUnitReporter jUnitReporter, IdProvider idProvider) { List runners = new ArrayList(); List exampleScenarios = cucumberExamples.createExampleScenarios(); for (CucumberScenario scenario : exampleScenarios) { try { - ExecutionUnitRunner exampleScenarioRunner = new ExecutionUnitRunner(runtime, scenario, jUnitReporter); + ExecutionUnitRunner exampleScenarioRunner = new ExecutionUnitRunner(runtime, scenario, jUnitReporter, idProvider); runners.add(exampleScenarioRunner); } catch (InitializationError initializationError) { initializationError.printStackTrace(); @@ -45,7 +47,7 @@ protected String getName() { @Override public Description getDescription() { if (description == null) { - description = Description.createSuiteDescription(getName(), cucumberExamples.getExamples()); + description = Description.createSuiteDescription(getName(), idProvider.next()); for (Runner child : getChildren()) { description.addChild(describeChild(child)); } diff --git a/junit/src/main/java/cucumber/runtime/junit/ExecutionUnitRunner.java b/junit/src/main/java/cucumber/runtime/junit/ExecutionUnitRunner.java index 2dee3b6b9a..5d82164e45 100644 --- a/junit/src/main/java/cucumber/runtime/junit/ExecutionUnitRunner.java +++ b/junit/src/main/java/cucumber/runtime/junit/ExecutionUnitRunner.java @@ -20,15 +20,17 @@ public class ExecutionUnitRunner extends ParentRunner { private final Runtime runtime; private final CucumberScenario cucumberScenario; private final JUnitReporter jUnitReporter; + private final IdProvider idProvider; private Description description; private final Map stepDescriptions = new HashMap(); private final List runnerSteps = new ArrayList(); - public ExecutionUnitRunner(Runtime runtime, CucumberScenario cucumberScenario, JUnitReporter jUnitReporter) throws InitializationError { + public ExecutionUnitRunner(Runtime runtime, CucumberScenario cucumberScenario, JUnitReporter jUnitReporter, IdProvider idProvider) throws InitializationError { super(ExecutionUnitRunner.class); this.runtime = runtime; this.cucumberScenario = cucumberScenario; this.jUnitReporter = jUnitReporter; + this.idProvider = idProvider; } public List getRunnerSteps() { @@ -53,21 +55,12 @@ public String getName() { @Override public Description getDescription() { if (description == null) { - description = Description.createSuiteDescription(getName(), cucumberScenario.getGherkinModel()); + description = Description.createSuiteDescription(getName(), idProvider.next()); if (cucumberScenario.getCucumberBackground() != null) { - for (Step backgroundStep : cucumberScenario.getCucumberBackground().getSteps()) { - // We need to make a copy of that step, so we have a unique one per scenario - Step copy = new Step( - backgroundStep.getComments(), - backgroundStep.getKeyword(), - backgroundStep.getName(), - backgroundStep.getLine(), - backgroundStep.getRows(), - backgroundStep.getDocString() - ); - description.addChild(describeChild(copy)); - runnerSteps.add(copy); + for (Step step : cucumberScenario.getCucumberBackground().getSteps()) { + description.addChild(describeChild(step)); + runnerSteps.add(step); } } @@ -89,7 +82,7 @@ protected Description describeChild(Step step) { } else { testName = step.getKeyword() + step.getName(); } - description = Description.createTestDescription(getName(), testName, step); + description = Description.createTestDescription(getName(), testName, idProvider.next()); stepDescriptions.put(step, description); } return description; diff --git a/junit/src/main/java/cucumber/runtime/junit/FeatureRunner.java b/junit/src/main/java/cucumber/runtime/junit/FeatureRunner.java index eb5928ab75..ce728edbc5 100644 --- a/junit/src/main/java/cucumber/runtime/junit/FeatureRunner.java +++ b/junit/src/main/java/cucumber/runtime/junit/FeatureRunner.java @@ -20,6 +20,7 @@ public class FeatureRunner extends ParentRunner { private final CucumberFeature cucumberFeature; private final Runtime runtime; + private final IdProvider idProvider; private final JUnitReporter jUnitReporter; private Description description; @@ -27,6 +28,7 @@ public FeatureRunner(CucumberFeature cucumberFeature, Runtime runtime, JUnitRepo super(null); this.cucumberFeature = cucumberFeature; this.runtime = runtime; + this.idProvider = new IdProvider(cucumberFeature.getPath()); this.jUnitReporter = jUnitReporter; buildFeatureElementRunners(); } @@ -40,7 +42,7 @@ public String getName() { @Override public Description getDescription() { if (description == null) { - description = Description.createSuiteDescription(getName(), cucumberFeature.getGherkinFeature()); + description = Description.createSuiteDescription(getName(), idProvider.next()); for (ParentRunner child : getChildren()) { description.addChild(describeChild(child)); } @@ -76,9 +78,9 @@ private void buildFeatureElementRunners() { try { ParentRunner featureElementRunner; if (cucumberTagStatement instanceof CucumberScenario) { - featureElementRunner = new ExecutionUnitRunner(runtime, (CucumberScenario) cucumberTagStatement, jUnitReporter); + featureElementRunner = new ExecutionUnitRunner(runtime, (CucumberScenario) cucumberTagStatement, jUnitReporter, idProvider); } else { - featureElementRunner = new ScenarioOutlineRunner(runtime, (CucumberScenarioOutline) cucumberTagStatement, jUnitReporter); + featureElementRunner = new ScenarioOutlineRunner(runtime, (CucumberScenarioOutline) cucumberTagStatement, jUnitReporter, idProvider); } children.add(featureElementRunner); } catch (InitializationError e) { diff --git a/junit/src/main/java/cucumber/runtime/junit/IdProvider.java b/junit/src/main/java/cucumber/runtime/junit/IdProvider.java new file mode 100644 index 0000000000..7ff1e17aad --- /dev/null +++ b/junit/src/main/java/cucumber/runtime/junit/IdProvider.java @@ -0,0 +1,56 @@ +package cucumber.runtime.junit; + +import java.io.Serializable; + +/** + * JUnit Descriptions require an unique id. However this id should be identical between test + * re-runs. This allows descriptions to be used to determine which failed tests should be rerun. + * + * IdProvider generates predictable unique ids. The feature name is used as a base to disambiguate + * between different features. + */ +class IdProvider { + + private long id = 0; + private final String base; + + IdProvider(String base) { + this.base = base; + } + + Serializable next() { + return new Id(base, id++); + } + + private static final class Id implements Serializable { + + private final String base; + private final long id; + + private Id(String base, long id) { + this.base = base; + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Id id1 = (Id) o; + return id == id1.id && base.equals(id1.base); + + } + + @Override + public int hashCode() { + int result = base.hashCode(); + result = 31 * result + (int) (id ^ (id >>> 32)); + return result; + } + } +} + + + + diff --git a/junit/src/main/java/cucumber/runtime/junit/ScenarioOutlineRunner.java b/junit/src/main/java/cucumber/runtime/junit/ScenarioOutlineRunner.java index bf947dc401..e1544f8644 100644 --- a/junit/src/main/java/cucumber/runtime/junit/ScenarioOutlineRunner.java +++ b/junit/src/main/java/cucumber/runtime/junit/ScenarioOutlineRunner.java @@ -15,18 +15,20 @@ public class ScenarioOutlineRunner extends Suite { private final CucumberScenarioOutline cucumberScenarioOutline; private final JUnitReporter jUnitReporter; + private final IdProvider idProvider; private Description description; - public ScenarioOutlineRunner(Runtime runtime, CucumberScenarioOutline cucumberScenarioOutline, JUnitReporter jUnitReporter) throws InitializationError { - super(null, buildRunners(runtime, cucumberScenarioOutline, jUnitReporter)); + public ScenarioOutlineRunner(Runtime runtime, CucumberScenarioOutline cucumberScenarioOutline, JUnitReporter jUnitReporter, IdProvider idProvider) throws InitializationError { + super(null, buildRunners(runtime, cucumberScenarioOutline, jUnitReporter, idProvider)); this.cucumberScenarioOutline = cucumberScenarioOutline; this.jUnitReporter = jUnitReporter; + this.idProvider = idProvider; } - private static List buildRunners(Runtime runtime, CucumberScenarioOutline cucumberScenarioOutline, JUnitReporter jUnitReporter) throws InitializationError { + private static List buildRunners(Runtime runtime, CucumberScenarioOutline cucumberScenarioOutline, JUnitReporter jUnitReporter, IdProvider idProvider) throws InitializationError { List runners = new ArrayList(); for (CucumberExamples cucumberExamples : cucumberScenarioOutline.getCucumberExamplesList()) { - runners.add(new ExamplesRunner(runtime, cucumberExamples, jUnitReporter)); + runners.add(new ExamplesRunner(runtime, cucumberExamples, jUnitReporter, idProvider)); } return runners; } @@ -39,7 +41,7 @@ public String getName() { @Override public Description getDescription() { if (description == null) { - description = Description.createSuiteDescription(getName(), cucumberScenarioOutline.getGherkinModel()); + description = Description.createSuiteDescription(getName(), idProvider.next()); for (Runner child : getChildren()) { description.addChild(describeChild(child)); } diff --git a/junit/src/test/java/cucumber/runtime/junit/ExecutionUnitRunnerTest.java b/junit/src/test/java/cucumber/runtime/junit/ExecutionUnitRunnerTest.java index c377295070..4c9df6be22 100644 --- a/junit/src/test/java/cucumber/runtime/junit/ExecutionUnitRunnerTest.java +++ b/junit/src/test/java/cucumber/runtime/junit/ExecutionUnitRunnerTest.java @@ -27,7 +27,8 @@ public void shouldAssignUnequalDescriptionsToDifferentOccurrencesOfSameStepInASc ExecutionUnitRunner runner = new ExecutionUnitRunner( null, (CucumberScenario) features.get(0).getFeatureElements().get(0), - createStandardJUnitReporter() + createStandardJUnitReporter(), + createStandardIdGenerator() ); // fish out the two occurrences of the same step and check whether we really got them @@ -55,7 +56,8 @@ public void shouldIncludeScenarioNameAsClassNameInStepDescriptions() throws Exce ExecutionUnitRunner runner = new ExecutionUnitRunner( null, (CucumberScenario) features.get(0).getFeatureElements().get(0), - createStandardJUnitReporter() + createStandardJUnitReporter(), + createStandardIdGenerator() ); // fish out the data from runner @@ -67,31 +69,6 @@ public void shouldIncludeScenarioNameAsClassNameInStepDescriptions() throws Exce assertEquals("description includes step keyword and name as method name", step.getKeyword() + step.getName(), stepDescription.getMethodName()); } - @Test - public void shouldPopulateRunnerStepsWithStepsUsedInStepDescriptions() throws Exception { - CucumberFeature cucumberFeature = TestFeatureBuilder.feature("featurePath", "" + - "Feature: feature name\n" + - " Background:\n" + - " Given background step\n" + - " Scenario:\n" + - " Then scenario name\n"); - - ExecutionUnitRunner runner = new ExecutionUnitRunner( - null, - (CucumberScenario) cucumberFeature.getFeatureElements().get(0), - createStandardJUnitReporter() - ); - - // fish out the data from runner - Description runnerDescription = runner.getDescription(); - Description backgroundStepDescription = runnerDescription.getChildren().get(0); - Description scenarioStepDescription = runnerDescription.getChildren().get(1); - Step runnerBackgroundStep = runner.getRunnerSteps().get(0); - Step runnerScenarioStep = runner.getRunnerSteps().get(1); - - assertDescriptionHasStepAsUniqueId(backgroundStepDescription, runnerBackgroundStep); - assertDescriptionHasStepAsUniqueId(scenarioStepDescription, runnerScenarioStep); - } @Test public void shouldUseScenarioNameForRunnerName() throws Exception { @@ -103,7 +80,8 @@ public void shouldUseScenarioNameForRunnerName() throws Exception { ExecutionUnitRunner runner = new ExecutionUnitRunner( null, (CucumberScenario) cucumberFeature.getFeatureElements().get(0), - createStandardJUnitReporter() + createStandardJUnitReporter(), + createStandardIdGenerator() ); assertEquals("Scenario: scenario name", runner.getName()); @@ -119,7 +97,8 @@ public void shouldUseStepKeyworkAndNameForChildName() throws Exception { ExecutionUnitRunner runner = new ExecutionUnitRunner( null, (CucumberScenario) cucumberFeature.getFeatureElements().get(0), - createStandardJUnitReporter() + createStandardJUnitReporter(), + createStandardIdGenerator() ); assertEquals("Then it works", runner.getDescription().getChildren().get(0).getMethodName()); @@ -135,19 +114,14 @@ public void shouldConvertTextFromFeatureFileForNamesWithFilenameCompatibleNameOp ExecutionUnitRunner runner = new ExecutionUnitRunner( null, (CucumberScenario) cucumberFeature.getFeatureElements().get(0), - createJUnitReporterWithOption("--filename-compatible-names") + createJUnitReporterWithOption("--filename-compatible-names"), + createStandardIdGenerator() ); assertEquals("Scenario__scenario_name", runner.getName()); assertEquals("Then_it_works", runner.getDescription().getChildren().get(0).getMethodName()); } - private void assertDescriptionHasStepAsUniqueId(Description stepDescription, Step step) { - // Note, JUnit uses the the serializable parameter (in this case the step) - // as the unique id when comparing Descriptions - assertEquals(stepDescription, Description.createTestDescription("", "", step)); - } - private JUnitReporter createStandardJUnitReporter() { return new JUnitReporter(null, null, false, new JUnitOptions(Collections.emptyList())); } @@ -155,4 +129,8 @@ private JUnitReporter createStandardJUnitReporter() { private JUnitReporter createJUnitReporterWithOption(String option) { return new JUnitReporter(null, null, false, new JUnitOptions(Arrays.asList(option))); } + + private IdProvider createStandardIdGenerator() { + return new IdProvider("dummy base"); + } } diff --git a/junit/src/test/java/cucumber/runtime/junit/FeatureRunnerTest.java b/junit/src/test/java/cucumber/runtime/junit/FeatureRunnerTest.java index 12a40eb742..a508147f33 100644 --- a/junit/src/test/java/cucumber/runtime/junit/FeatureRunnerTest.java +++ b/junit/src/test/java/cucumber/runtime/junit/FeatureRunnerTest.java @@ -8,13 +8,18 @@ import cucumber.runtime.io.ClasspathResourceLoader; import cucumber.runtime.model.CucumberFeature; import org.junit.Test; +import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.InitializationError; +import java.io.IOException; import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; public class FeatureRunnerTest { @@ -143,4 +148,55 @@ private String runFeatureWithFormatterSpy(CucumberFeature cucumberFeature) throw return formatterSpy.toString(); } + @Test + public void shouldPopulateDescriptionsWithStableUniqueIds() throws Exception { + FeatureRunner runner = createFeatureRunner(); + FeatureRunner rerunner = createFeatureRunner(); + + Set descriptions = new HashSet(); + assertDescriptionIsUnique(runner.getDescription(), descriptions); + assertDescriptionIsPredictable(rerunner.getDescription(), descriptions); + } + + private FeatureRunner createFeatureRunner() throws IOException, InitializationError { + CucumberFeature cucumberFeature = TestFeatureBuilder.feature("featurePath", "" + + "Feature: feature name\n" + + " Background:\n" + + " Given background step\n" + + " Scenario: A\n" + + " Then scenario name\n" + + " Scenario: B\n" + + " Then scenario name\n" + + " Scenario Outline: C\n" + + " Then scenario \n" + + " Examples:\n" + + " | name |\n" + + " | C |\n" + + " | D |\n" + + " | E |\n" + + ); + + return new FeatureRunner(cucumberFeature, null, createStandardJUnitReporter()); + } + + private static void assertDescriptionIsUnique(Description description, Set descriptions) { + // Note, JUnit uses the the serializable parameter (in this case the step) + // as the unique id when comparing Descriptions + assertTrue(descriptions.add(description)); + for (Description each : description.getChildren()) { + assertDescriptionIsUnique(each, descriptions); + } + } + + private static void assertDescriptionIsPredictable(Description description, Set descriptions) { + assertTrue(descriptions.contains(description)); + for (Description each : description.getChildren()) { + assertDescriptionIsPredictable(each, descriptions); + } + } + + private static JUnitReporter createStandardJUnitReporter() { + return new JUnitReporter(null, null, false, new JUnitOptions(Collections.emptyList())); + } }