From 11ac9ff65455d52779f4f39448c630ffdfce160b Mon Sep 17 00:00:00 2001 From: brasmusson Date: Sat, 1 Jun 2013 21:09:09 +0200 Subject: [PATCH 1/5] Make it possible to inject a glue when creating a Runtime To aid testing, make it possible to inject a glue when creating a Runtime. Also add a test specify that a CucumberException is thrown if no backends are found (or an emtpy collection of backends is injected). --- core/src/main/java/cucumber/runtime/Runtime.java | 13 +++++++++---- .../test/java/cucumber/runtime/RuntimeTest.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/cucumber/runtime/Runtime.java b/core/src/main/java/cucumber/runtime/Runtime.java index 7c9c3da39b..0ab99d7f96 100644 --- a/core/src/main/java/cucumber/runtime/Runtime.java +++ b/core/src/main/java/cucumber/runtime/Runtime.java @@ -51,19 +51,24 @@ public Runtime(ResourceLoader resourceLoader, ClassLoader classLoader, RuntimeOp } public Runtime(ResourceLoader resourceLoader, ClassLoader classLoader, Collection backends, RuntimeOptions runtimeOptions) { - this.resourceLoader = resourceLoader; - this.classLoader = classLoader; + this(resourceLoader, classLoader, backends, runtimeOptions, null); + } + + public Runtime(ResourceLoader resourceLoader, ClassLoader classLoader, Collection backends, + RuntimeOptions runtimeOptions, RuntimeGlue optionalGlue) { if (backends.isEmpty()) { throw new CucumberException("No backends were found. Please make sure you have a backend module on your CLASSPATH."); } + this.resourceLoader = resourceLoader; + this.classLoader = classLoader; this.backends = backends; - glue = new RuntimeGlue(undefinedStepsTracker, new LocalizedXStreams(classLoader)); + this.runtimeOptions = runtimeOptions; + this.glue = optionalGlue != null ? optionalGlue : new RuntimeGlue(undefinedStepsTracker, new LocalizedXStreams(classLoader)); for (Backend backend : backends) { backend.loadGlue(glue, runtimeOptions.getGlue()); backend.setUnreportedStepExecutor(this); } - this.runtimeOptions = runtimeOptions; } private static Collection loadBackends(ResourceLoader resourceLoader, ClassLoader classLoader) { diff --git a/core/src/test/java/cucumber/runtime/RuntimeTest.java b/core/src/test/java/cucumber/runtime/RuntimeTest.java index bc092ceb3b..fe70ab34fb 100644 --- a/core/src/test/java/cucumber/runtime/RuntimeTest.java +++ b/core/src/test/java/cucumber/runtime/RuntimeTest.java @@ -13,12 +13,14 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Properties; import static cucumber.runtime.TestHelper.feature; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; public class RuntimeTest { @@ -163,6 +165,18 @@ public void strict_with_errors() { assertEquals(0x1, runtime.exitStatus()); } + @Test + public void should_throw_cucumer_exception_if_no_backends_are_found() throws Exception { + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + new Runtime(new ClasspathResourceLoader(classLoader), classLoader, Collections.emptyList(), + new RuntimeOptions(new Properties())); + fail("A CucumberException should have been thrown"); + } catch (CucumberException e) { + assertEquals("No backends were found. Please make sure you have a backend module on your CLASSPATH.", e.getMessage()); + } + } + private Runtime createStrictRuntime() { return createRuntime("-g", "anything", "--strict"); } From 87b2ee4029ddfc25f6e68242dd6e103a7be7bc57 Mon Sep 17 00:00:00 2001 From: brasmusson Date: Fri, 21 Jun 2013 22:02:02 +0200 Subject: [PATCH 2/5] Align the step results in the JUnit test result files Adjust the number of . separating the step text from the step result, so that the results are aligned. --- .../runtime/formatter/JUnitFormatter.java | 4 +- .../formatter/JUnitFormatterTest_2.report.xml | 12 +++--- .../formatter/JUnitFormatterTest_3.report.xml | 38 +++++++++---------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java b/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java index 9d58a1de64..ed4d17ae28 100644 --- a/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java +++ b/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java @@ -224,7 +224,9 @@ public void updateElement(Document doc, Element tc) { if ("undefined".equals(result.getStatus()) || "pending".equals(result.getStatus())) skipped = result; sb.append(steps.get(i).getKeyword()); sb.append(steps.get(i).getName()); - for (int j = 0; sb.length() - length + j < 140; j++) sb.append("."); + do { + sb.append("."); + } while (sb.length() - length < 76); sb.append(result.getStatus()); sb.append("\n"); } diff --git a/core/src/test/resources/cucumber/runtime/formatter/JUnitFormatterTest_2.report.xml b/core/src/test/resources/cucumber/runtime/formatter/JUnitFormatterTest_2.report.xml index e9e228902d..55c7f91706 100644 --- a/core/src/test/resources/cucumber/runtime/formatter/JUnitFormatterTest_2.report.xml +++ b/core/src/test/resources/cucumber/runtime/formatter/JUnitFormatterTest_2.report.xml @@ -1,18 +1,18 @@ - - - - - - From ebfb7aaf5e1cbb8e2d2bd21c1a8ec6b4daab9e6e Mon Sep 17 00:00:00 2001 From: brasmusson Date: Wed, 19 Jun 2013 16:20:37 +0200 Subject: [PATCH 3/5] Add tests for the JUnitFormatter to disclose issues The main issue is that the JUnitFormatter does not handle failures in the before and after hooks properly. To add flexibility the tests also specify that the JUnitFormatter should handle both all-steps-first execution and one-step-at-the-time execution. --- .../runtime/formatter/JUnitFormatter.java | 1 + .../java/cucumber/runtime/TestHelper.java | 2 +- .../runtime/formatter/JUnitFormatterTest.java | 448 ++++++++++++++++++ .../runtime/formatter/StepMatcher.java | 18 + 4 files changed, 468 insertions(+), 1 deletion(-) create mode 100755 core/src/test/java/cucumber/runtime/formatter/StepMatcher.java diff --git a/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java b/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java index ed4d17ae28..fa772d5788 100644 --- a/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java +++ b/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java @@ -46,6 +46,7 @@ class JUnitFormatter implements Formatter, Reporter, StrictAware { public JUnitFormatter(URL out) throws IOException { this.out = new UTF8OutputStreamWriter(new URLOutputStream(out)); + TestCase.treatSkippedAsFailure = false; try { doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); rootElement = doc.createElement("testsuite"); diff --git a/core/src/test/java/cucumber/runtime/TestHelper.java b/core/src/test/java/cucumber/runtime/TestHelper.java index 8ed63a6420..34ad28b9b7 100644 --- a/core/src/test/java/cucumber/runtime/TestHelper.java +++ b/core/src/test/java/cucumber/runtime/TestHelper.java @@ -12,7 +12,7 @@ @Ignore public class TestHelper { - static CucumberFeature feature(final String path, final String source) throws IOException { + public static CucumberFeature feature(final String path, final String source) throws IOException { ArrayList cucumberFeatures = new ArrayList(); FeatureBuilder featureBuilder = new FeatureBuilder(cucumberFeatures); featureBuilder.parse(new Resource() { diff --git a/core/src/test/java/cucumber/runtime/formatter/JUnitFormatterTest.java b/core/src/test/java/cucumber/runtime/formatter/JUnitFormatterTest.java index 84db0a9a5f..0e9adf9a27 100644 --- a/core/src/test/java/cucumber/runtime/formatter/JUnitFormatterTest.java +++ b/core/src/test/java/cucumber/runtime/formatter/JUnitFormatterTest.java @@ -1,27 +1,60 @@ package cucumber.runtime.formatter; +import cucumber.api.PendingException; import cucumber.runtime.Backend; +import cucumber.runtime.HookDefinition; import cucumber.runtime.Runtime; +import cucumber.runtime.RuntimeGlue; import cucumber.runtime.RuntimeOptions; +import cucumber.runtime.StepDefinitionMatch; +import cucumber.runtime.TestHelper; +import cucumber.runtime.Utils; import cucumber.runtime.io.ClasspathResourceLoader; +import cucumber.runtime.model.CucumberFeature; +import gherkin.I18n; +import gherkin.formatter.model.Feature; +import gherkin.formatter.model.Match; +import gherkin.formatter.model.Result; +import gherkin.formatter.model.Scenario; import gherkin.formatter.model.Step; +import gherkin.formatter.model.Tag; + import org.custommonkey.xmlunit.Diff; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import java.io.File; +import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Properties; +import java.util.Scanner; +import java.util.Set; + +import junit.framework.AssertionFailedError; import static java.util.Arrays.asList; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyCollectionOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -52,6 +85,288 @@ public void featureSimpleStrictTest() throws Exception { assertXmlEqual("cucumber/runtime/formatter/JUnitFormatterTest_1_strict.report.xml", report); } + @Test + public void should_format_passed_scenario() throws Throwable { + CucumberFeature feature = TestHelper.feature("path/test.feature", + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n" + + " When second step\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", "passed"); + stepsToResult.put("second step", "passed"); + stepsToResult.put("third step", "passed"); + + String formatterOutput = runFeatureWithJUnitFormatter(feature, stepsToResult); + + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(formatterOutput)); + } + + @Test + public void should_format_pending_scenario() throws Throwable { + CucumberFeature feature = TestHelper.feature("path/test.feature", + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n" + + " When second step\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", "pending"); + stepsToResult.put("second step", "skipped"); + stepsToResult.put("third step", "undefined"); + + String formatterOutput = runFeatureWithJUnitFormatter(feature, stepsToResult); + + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(formatterOutput)); + } + + @Test + public void should_format_failed_scenario() throws Throwable { + CucumberFeature feature = TestHelper.feature("path/test.feature", + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n" + + " When second step\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", "passed"); + stepsToResult.put("second step", "passed"); + stepsToResult.put("third step", "failed"); + + String formatterOutput = runFeatureWithJUnitFormatter(feature, stepsToResult); + + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(formatterOutput)); + } + + @Test + public void should_handle_failure_in_before_hook() throws Throwable { + CucumberFeature feature = TestHelper.feature("path/test.feature", + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n" + + " When second step\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", "passed"); + stepsToResult.put("second step", "passed"); + stepsToResult.put("third step", "passed"); + Set hooksFailures = new HashSet(); + hooksFailures.add("before"); + + String formatterOutput = runFeatureWithJUnitFormatter(feature, stepsToResult, hooksFailures); + + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(formatterOutput)); + } + + @Test + public void should_handle_failure_in_before_hook_with_background() throws Throwable { + CucumberFeature feature = TestHelper.feature("path/test.feature", + "Feature: feature name\n" + + " Background: background name\n" + + " Given first step\n" + + " Scenario: scenario name\n" + + " When second step\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", "passed"); + stepsToResult.put("second step", "passed"); + stepsToResult.put("third step", "passed"); + Set hooksFailures = new HashSet(); + hooksFailures.add("before"); + + String formatterOutput = runFeatureWithJUnitFormatter(feature, stepsToResult, hooksFailures); + + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(formatterOutput)); + } + + @Test + public void should_handle_failure_in_after_hook() throws Throwable { + CucumberFeature feature = TestHelper.feature("path/test.feature", + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n" + + " When second step\n" + + " Then third step\n"); + Map stepsToResult = new HashMap(); + stepsToResult.put("first step", "passed"); + stepsToResult.put("second step", "passed"); + stepsToResult.put("third step", "passed"); + Set hooksFailures = new HashSet(); + hooksFailures.add("after"); + + String formatterOutput = runFeatureWithJUnitFormatter(feature, stepsToResult, hooksFailures); + + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(formatterOutput)); + } + + @Test + public void should_accumulate_time_from_steps_and_hooks() throws Exception { + final File report = File.createTempFile("cucumber-jvm-junit", ".xml"); + final JUnitFormatter junitFormatter = createJUnitFormatter(report); + + junitFormatter.uri(uri()); + junitFormatter.feature(feature("feature name")); + junitFormatter.before(match(), result("passed", milliSeconds(1))); + junitFormatter.scenario(scenario("scenario name")); + junitFormatter.step(step("keyword ", "step name")); + junitFormatter.match(match()); + junitFormatter.result(result("passed", milliSeconds(1))); + junitFormatter.step(step("keyword ", "step name")); + junitFormatter.match(match()); + junitFormatter.result(result("passed", milliSeconds(1))); + junitFormatter.after(match(), result("passed", milliSeconds(1))); + junitFormatter.eof(); + junitFormatter.done(); + junitFormatter.close(); + + String actual = new Scanner(new FileInputStream(report), "UTF-8").useDelimiter("\\A").next(); + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, actual); + } + + @Test + public void should_handle_all_step_calls_first_execution() throws Exception { + final File report = File.createTempFile("cucumber-jvm-junit", ".xml"); + final JUnitFormatter junitFormatter = createJUnitFormatter(report); + + junitFormatter.uri(uri()); + junitFormatter.feature(feature("feature name")); + junitFormatter.scenario(scenario("scenario name")); + junitFormatter.step(step("keyword ", "step name")); + junitFormatter.step(step("keyword ", "step name")); + junitFormatter.match(match()); + junitFormatter.result(result("passed")); + junitFormatter.match(match()); + junitFormatter.result(result("passed")); + junitFormatter.eof(); + junitFormatter.done(); + junitFormatter.close(); + + String actual = new Scanner(new FileInputStream(report), "UTF-8").useDelimiter("\\A").next(); + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(actual)); + } + + @Test + public void should_handle_one_step_at_the_time_execution() throws Exception { + final File report = File.createTempFile("cucumber-jvm-junit", ".xml"); + final JUnitFormatter junitFormatter = createJUnitFormatter(report); + + junitFormatter.uri(uri()); + junitFormatter.feature(feature("feature name")); + junitFormatter.scenario(scenario("scenario name")); + junitFormatter.step(step("keyword ", "step name")); + junitFormatter.match(match()); + junitFormatter.result(result("passed")); + junitFormatter.step(step("keyword ", "step name")); + junitFormatter.match(match()); + junitFormatter.result(result("passed")); + junitFormatter.eof(); + junitFormatter.done(); + junitFormatter.close(); + + String actual = new Scanner(new FileInputStream(report), "UTF-8").useDelimiter("\\A").next(); + String expected = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + assertXmlEqual(expected, replaceTimeWithZeroTime(actual)); + } + private File runFeaturesWithJunitFormatter(final List featurePaths) throws IOException { return runFeaturesWithJunitFormatter(featurePaths, false); } @@ -77,11 +392,144 @@ private File runFeaturesWithJunitFormatter(final List featurePaths, bool return report; } + private String runFeatureWithJUnitFormatter(final CucumberFeature feature, final Map stepsToResult) + throws Throwable { + return runFeatureWithJUnitFormatter(feature, stepsToResult, Collections.emptySet()); + } + + private String runFeatureWithJUnitFormatter(final CucumberFeature feature, final Map stepsToResult, + final Set hookFailures) throws Throwable { + final RuntimeOptions runtimeOptions = new RuntimeOptions(new Properties()); + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + final ClasspathResourceLoader resourceLoader = new ClasspathResourceLoader(classLoader); + final RuntimeGlue glue = createMockedRuntimeGlueThatMatchesTheSteps(stepsToResult, hookFailures); + final Runtime runtime = new Runtime(resourceLoader, classLoader, asList(mock(Backend.class)), runtimeOptions, glue); + final File report = File.createTempFile("cucumber-jvm-junit", ".xml"); + final JUnitFormatter junitFormatter = createJUnitFormatter(report); + + feature.run(junitFormatter, junitFormatter, runtime); + junitFormatter.done(); + junitFormatter.close(); + + return new Scanner(new FileInputStream(report), "UTF-8").useDelimiter("\\A").next(); + } + private void assertXmlEqual(String expectedPath, File actual) throws IOException, ParserConfigurationException, SAXException { XMLUnit.setIgnoreWhitespace(true); InputStreamReader control = new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream(expectedPath), "UTF-8"); Diff diff = new Diff(control, new FileReader(actual)); + assertTrue("XML files are similar " + diff, diff.identical()); } + + private void assertXmlEqual(String expected, String actual) throws SAXException, IOException { + XMLUnit.setIgnoreWhitespace(true); + Diff diff = new Diff(expected, actual); assertTrue("XML files are similar " + diff, diff.identical()); } + private JUnitFormatter createJUnitFormatter(final File report) throws IOException { + return new JUnitFormatter(Utils.toURL(report.getAbsolutePath())); + } + + private RuntimeGlue createMockedRuntimeGlueThatMatchesTheSteps(Map stepsToResult, + final Set hookFailures) throws Throwable { + RuntimeGlue glue = mock(RuntimeGlue.class); + mockSteps(glue, stepsToResult); + mockHooks(glue, hookFailures); + return glue; + } + + private void mockSteps(RuntimeGlue glue, Map stepsToResult) throws Throwable { + for (String stepName : stepsToResult.keySet()) { + if (!"undefined".equals(stepsToResult.get(stepName))) { + StepDefinitionMatch matchStep = mock(StepDefinitionMatch.class); + when(glue.stepDefinitionMatch(anyString(), stepWithName(stepName), (I18n)any())).thenReturn(matchStep); + if ("pending".equals(stepsToResult.get(stepName))) { + doThrow(new PendingException()).when(matchStep).runStep((I18n)any()); + } else if ("failed".equals(stepsToResult.get(stepName))) { + AssertionFailedError error = mockAssertionFailedError(); + doThrow(error).when(matchStep).runStep((I18n)any()); + } else if (!"passed".equals(stepsToResult.get(stepName)) && + !"skipped".equals(stepsToResult.get(stepName))) { + fail("Cannot mock step to the result: " + stepsToResult.get(stepName)); + } + } + } + } + + private void mockHooks(RuntimeGlue glue, final Set hookFailures) throws Throwable { + for (String hookType : hookFailures) { + HookDefinition hook = mock(HookDefinition.class); + when(hook.matches(anyCollectionOf(Tag.class))).thenReturn(true); + AssertionFailedError error = mockAssertionFailedError(); + doThrow(error).when(hook).execute((cucumber.api.Scenario)any()); + if ("before".equals(hookType)) { + when(glue.getBeforeHooks()).thenReturn(Arrays.asList(hook)); + } else if ("after".equals(hookType)) { + when(glue.getAfterHooks()).thenReturn(Arrays.asList(hook)); + } else { + fail("Only before and after hooks are allowed, hook type found was: " + hookType); + } + } + } + + private Step stepWithName(String name) { + return argThat(new StepMatcher(name)); + } + + private AssertionFailedError mockAssertionFailedError() { + AssertionFailedError error = mock(AssertionFailedError.class); + Answer printStackTraceHandler = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + PrintWriter writer = (PrintWriter) invocation.getArguments()[0]; + writer.print("the stack trace"); + return null; + } + }; + doAnswer(printStackTraceHandler).when(error).printStackTrace((PrintWriter)any()); + return error; + } + + private String replaceTimeWithZeroTime(String formatterOutput) { + return formatterOutput.replaceAll("time=\".*\"", "time=\"0.0\""); + } + + private String uri() { + return "uri"; + } + + private Feature feature(String featureName) { + Feature feature = mock(Feature.class); + when(feature.getName()).thenReturn(featureName); + return feature; + } + + private Scenario scenario(String scenarioName) { + Scenario scenario = mock(Scenario.class); + when(scenario.getName()).thenReturn(scenarioName); + return scenario; + } + + private Step step(String keyword, String stepName) { + Step step = mock(Step.class); + when(step.getKeyword()).thenReturn(keyword); + when(step.getName()).thenReturn(stepName); + return step; + } + + private Match match() { + return mock(Match.class); + } + + private Result result(String status) { + return result(status, null); + } + + private Result result(String status, Long duration) { + return new Result(status, duration, null); + } + + private Long milliSeconds(int milliSeconds) { + return milliSeconds * 1000000L; + } } diff --git a/core/src/test/java/cucumber/runtime/formatter/StepMatcher.java b/core/src/test/java/cucumber/runtime/formatter/StepMatcher.java new file mode 100755 index 0000000000..8bccd70f62 --- /dev/null +++ b/core/src/test/java/cucumber/runtime/formatter/StepMatcher.java @@ -0,0 +1,18 @@ +package cucumber.runtime.formatter; + +import gherkin.formatter.model.Step; + +import org.mockito.ArgumentMatcher; + +public class StepMatcher extends ArgumentMatcher { + private final String nameToMatch; + + public StepMatcher(String name) { + this.nameToMatch = name; + } + + @Override + public boolean matches(Object argument) { + return argument instanceof Step && nameToMatch.endsWith(((Step)argument).getName()); + } +} From 37d9bcb524473c280bc624be12cbef0b45f08e39 Mon Sep 17 00:00:00 2001 From: brasmusson Date: Thu, 20 Jun 2013 11:13:03 +0200 Subject: [PATCH 4/5] Fix issues in the JUnitFormatter Change the JUnitFormatter to handle failures in the before and after hooks properly. Also change the JUnitFormatter to handle both all-steps-first execution and one-step-at-the-time execution. --- .../runtime/formatter/JUnitFormatter.java | 114 ++++++++++++------ 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java b/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java index fa772d5788..aaef83f6ba 100644 --- a/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java +++ b/core/src/main/java/cucumber/runtime/formatter/JUnitFormatter.java @@ -63,13 +63,15 @@ public void feature(Feature feature) { @Override public void background(Background background) { - testCase = new TestCase(); - root = testCase.createElement(doc); + if (!isCurrentTestCaseCreatedNameless()) { + testCase = new TestCase(); + root = testCase.createElement(doc); + } } @Override public void scenario(Scenario scenario) { - if (testCase != null && testCase.scenario == null) { + if (isCurrentTestCaseCreatedNameless()) { testCase.scenario = scenario; } else { testCase = new TestCase(scenario); @@ -81,6 +83,10 @@ public void scenario(Scenario scenario) { increaseAttributeValue(rootElement, "tests"); } + private boolean isCurrentTestCaseCreatedNameless() { + return testCase != null && testCase.scenario == null; + } + @Override public void step(Step step) { if (testCase != null) testCase.steps.add(step); @@ -111,6 +117,10 @@ public void result(Result result) { @Override public void before(Match match, Result result) { + if (!isCurrentTestCaseCreatedNameless()) { + testCase = new TestCase(); + root = testCase.createElement(doc); + } handleHook(result); } @@ -120,10 +130,8 @@ public void after(Match match, Result result) { } private void handleHook(Result result) { - if (result.getStatus().equals(Result.FAILED)) { - testCase.results.add(result); - } - + testCase.hookResults.add(result); + testCase.updateElement(doc, root); } private void increaseAttributeValue(Element element, String attribute) { @@ -136,6 +144,7 @@ private void increaseAttributeValue(Element element, String attribute) { @Override public void scenarioOutline(ScenarioOutline scenarioOutline) { + testCase = null; } @Override @@ -196,6 +205,7 @@ private TestCase() { static boolean treatSkippedAsFailure = false; final List steps = new ArrayList(); final List results = new ArrayList(); + final List hookResults = new ArrayList(); private Element createElement(Document doc) { return doc.createElement("testcase"); @@ -207,51 +217,31 @@ private void writeElement(Document doc, Element tc) { } public void updateElement(Document doc, Element tc) { - long totalDurationNanos = 0; - for (Result r : results) { - totalDurationNanos += r.getDuration() == null ? 0 : r.getDuration(); - } - - double totalDurationSeconds = ((double) totalDurationNanos) / 1000000000; - String time = NUMBER_FORMAT.format(totalDurationSeconds); - tc.setAttribute("time", time); + tc.setAttribute("time", calculateTotalDurationString()); StringBuilder sb = new StringBuilder(); + addStepAndResultListing(sb); Result skipped = null, failed = null; - for (int i = 0; i < steps.size(); i++) { - int length = sb.length(); - Result result = results.get(i); + for (Result result : results) { if ("failed".equals(result.getStatus())) failed = result; if ("undefined".equals(result.getStatus()) || "pending".equals(result.getStatus())) skipped = result; - sb.append(steps.get(i).getKeyword()); - sb.append(steps.get(i).getName()); - do { - sb.append("."); - } while (sb.length() - length < 76); - sb.append(result.getStatus()); - sb.append("\n"); + } + for (Result result : hookResults) { + if (failed == null && "failed".equals(result.getStatus())) failed = result; } Element child; if (failed != null) { - sb.append("\nStackTrace:\n"); - StringWriter sw = new StringWriter(); - failed.getError().printStackTrace(new PrintWriter(sw)); - sb.append(sw.toString()); - child = doc.createElement("failure"); - child.setAttribute("message", failed.getErrorMessage()); - child.appendChild(doc.createCDATASection(sb.toString())); + addStackTrace(sb, failed); + child = createElementWithMessage(doc, sb, "failure", failed.getErrorMessage()); } else if (skipped != null) { if (treatSkippedAsFailure) { - child = doc.createElement("failure"); - child.setAttribute("message", "The scenario has pending or undefined step(s)"); + child = createElementWithMessage(doc, sb, "failure", "The scenario has pending or undefined step(s)"); } else { - child = doc.createElement("skipped"); + child = createElement(doc, sb, "skipped"); } - child.appendChild(doc.createCDATASection(sb.toString())); } else { - child = doc.createElement("system-out"); - child.appendChild(doc.createCDATASection(sb.toString())); + child = createElement(doc, sb, "system-out"); } Node existingChild = tc.getFirstChild(); @@ -262,6 +252,54 @@ public void updateElement(Document doc, Element tc) { } } + private String calculateTotalDurationString() { + long totalDurationNanos = 0; + for (Result r : results) { + totalDurationNanos += r.getDuration() == null ? 0 : r.getDuration(); + } + for (Result r : hookResults) { + totalDurationNanos += r.getDuration() == null ? 0 : r.getDuration(); + } + double totalDurationSeconds = ((double) totalDurationNanos) / 1000000000; + return NUMBER_FORMAT.format(totalDurationSeconds); + } + + private void addStepAndResultListing(StringBuilder sb) { + for (int i = 0; i < steps.size(); i++) { + int length = sb.length(); + String resultStatus = "not executed"; + if (i < results.size()) { + resultStatus = results.get(i).getStatus(); + } + sb.append(steps.get(i).getKeyword()); + sb.append(steps.get(i).getName()); + do { + sb.append("."); + } while (sb.length() - length < 76); + sb.append(resultStatus); + sb.append("\n"); + } + } + + private void addStackTrace(StringBuilder sb, Result failed) { + sb.append("\nStackTrace:\n"); + StringWriter sw = new StringWriter(); + failed.getError().printStackTrace(new PrintWriter(sw)); + sb.append(sw.toString()); + } + + private Element createElementWithMessage(Document doc, StringBuilder sb, String elementType, String message) { + Element child = createElement(doc, sb, elementType); + child.setAttribute("message", message); + return child; + } + + private Element createElement(Document doc, StringBuilder sb, String elementType) { + Element child = doc.createElement(elementType); + child.appendChild(doc.createCDATASection(sb.toString())); + return child; + } + } } From 1622c698a37d0095fe78e73c54379ccd2a80de9c Mon Sep 17 00:00:00 2001 From: brasmusson Date: Sun, 14 Jul 2013 12:07:34 +0200 Subject: [PATCH 5/5] Revert to all-steps-first scenario execution To fix the missmatch between the PrettyFormatter from the Gherkin library and Cucumber-JVM, by reverting the scenario execution to all-steps-first execution as in Cucumber-JVM 1.1.2. This is possible since the JSONFormatter in the Gherkin library in Gherkin 2.12.0 supports all-steps-first execution. --- .../java/cucumber/runtime/model/CucumberScenario.java | 4 ++-- .../runtime/model/CucumberScenarioOutline.java | 1 - .../java/cucumber/runtime/model/StepContainer.java | 10 +++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/cucumber/runtime/model/CucumberScenario.java b/core/src/main/java/cucumber/runtime/model/CucumberScenario.java index 8a49fefd05..631752474c 100644 --- a/core/src/main/java/cucumber/runtime/model/CucumberScenario.java +++ b/core/src/main/java/cucumber/runtime/model/CucumberScenario.java @@ -37,7 +37,7 @@ public void run(Formatter formatter, Reporter reporter, Runtime runtime) { runBackground(formatter, reporter, runtime); format(formatter); - runSteps(formatter, reporter, runtime); + runSteps(reporter, runtime); runtime.runAfterHooks(reporter, tags); runtime.disposeBackendWorlds(); @@ -46,7 +46,7 @@ public void run(Formatter formatter, Reporter reporter, Runtime runtime) { private void runBackground(Formatter formatter, Reporter reporter, Runtime runtime) { if (cucumberBackground != null) { cucumberBackground.format(formatter); - cucumberBackground.runSteps(formatter, reporter, runtime); + cucumberBackground.runSteps(reporter, runtime); } } } diff --git a/core/src/main/java/cucumber/runtime/model/CucumberScenarioOutline.java b/core/src/main/java/cucumber/runtime/model/CucumberScenarioOutline.java index 85a1fa554c..ab81237646 100644 --- a/core/src/main/java/cucumber/runtime/model/CucumberScenarioOutline.java +++ b/core/src/main/java/cucumber/runtime/model/CucumberScenarioOutline.java @@ -38,7 +38,6 @@ public List getCucumberExamplesList() { @Override public void run(Formatter formatter, Reporter reporter, Runtime runtime) { format(formatter); - formatSteps(formatter); for (CucumberExamples cucumberExamples : cucumberExamplesList) { cucumberExamples.format(formatter); List exampleScenarios = cucumberExamples.createExampleScenarios(); diff --git a/core/src/main/java/cucumber/runtime/model/StepContainer.java b/core/src/main/java/cucumber/runtime/model/StepContainer.java index abe02b40a1..e82648054b 100644 --- a/core/src/main/java/cucumber/runtime/model/StepContainer.java +++ b/core/src/main/java/cucumber/runtime/model/StepContainer.java @@ -29,22 +29,18 @@ public void step(Step step) { void format(Formatter formatter) { statement.replay(formatter); - } - - void formatSteps(Formatter formatter) { for (Step step : getSteps()) { formatter.step(step); } } - void runSteps(Formatter formatter, Reporter reporter, Runtime runtime) { + void runSteps(Reporter reporter, Runtime runtime) { for (Step step : getSteps()) { - runStep(step, formatter, reporter, runtime); + runStep(step, reporter, runtime); } } - void runStep(Step step, Formatter formatter, Reporter reporter, Runtime runtime) { - formatter.step(step); + void runStep(Step step, Reporter reporter, Runtime runtime) { runtime.runStep(cucumberFeature.getUri(), step, reporter, cucumberFeature.getI18n()); } }