From b5b6f2b41918a71f6401b4448742d6fcf49b825f Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 2 Nov 2019 22:04:27 +0100 Subject: [PATCH] [Core] Implement TeamCity output format plugin This plugin inserts markers that can be picked up by IDEA and Teamcity. These in turn can then use this information to display a test tree and show the output grouped by scenario. This is plugin is nessesary because Intelij keeps using reflection to extract more information from the plugin system then what would normally be available. By substituting the CucumberJvm[1-5]SMFormatter with this plugin we avoid runtime errors. Unfortunately the teamcity format is poorly documented[1]. Reverse engingeering[2][3] the format from the plugin yields some information but the exact function is rather opaque. 1. https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-MessageFlowId 2. https://github.com/JetBrains/intellij-community/tree/master/plugins/cucumber-jvm-formatter5/src/org/jetbrains/plugins/cucumber/java/run 3. https://github.com/mpkorstanje/intellij-community/blob/master/plugins/cucumber-jvm-formatter/src/org/jetbrains/plugins/cucumber/java/run --- .../cucumber/core/options/PluginOption.java | 18 +- .../cucumber/core/plugin/TeamCityPlugin.java | 379 ++++++++++++++++++ .../runner/PickleStepDefinitionMatch.java | 2 +- .../java/io/cucumber/core/runner/Runner.java | 2 +- .../core/plugin/CanonicalEventOrderTest.java | 2 +- .../core/plugin/TeamCityPluginTest.java | 140 +++++++ .../messages/GherkinMessagesExamples.java | 16 +- .../messages/GherkinMessagesFeature.java | 21 +- .../gherkin/messages/GherkinMessagesRule.java | 21 +- .../GherkinMessagesScenarioOutline.java | 13 +- .../vintage/GherkinVintageExamples.java | 24 +- .../vintage/GherkinVintageFeature.java | 17 +- .../gherkin/vintage/GherkinVintagePickle.java | 23 +- .../GherkinVintageScenarioOutline.java | 13 +- .../io/cucumber/core/gherkin/Container.java | 6 +- .../java8/AbstractGlueDefinition.java | 2 +- .../engine/TestCaseResultObserverTest.java | 2 +- ...UnitReporterWithStepNotificationsTest.java | 4 +- .../plugin/event/SnippetsSuggestedEvent.java | 4 +- .../testng/TestCaseResultListenerTest.java | 4 +- 20 files changed, 643 insertions(+), 70 deletions(-) create mode 100644 core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java create mode 100755 core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java diff --git a/core/src/main/java/io/cucumber/core/options/PluginOption.java b/core/src/main/java/io/cucumber/core/options/PluginOption.java index e974dc00ac..0f22a6a6a3 100644 --- a/core/src/main/java/io/cucumber/core/options/PluginOption.java +++ b/core/src/main/java/io/cucumber/core/options/PluginOption.java @@ -12,6 +12,7 @@ import io.cucumber.core.plugin.PrettyFormatter; import io.cucumber.core.plugin.ProgressFormatter; import io.cucumber.core.plugin.RerunFormatter; +import io.cucumber.core.plugin.TeamCityPlugin; import io.cucumber.core.plugin.TestNGFormatter; import io.cucumber.core.plugin.TimelineFormatter; import io.cucumber.core.plugin.UnusedStepsSummaryPrinter; @@ -44,15 +45,16 @@ public class PluginOption implements Options.Plugin { put("timeline", TimelineFormatter.class); put("unused", UnusedStepsSummaryPrinter.class); put("usage", UsageFormatter.class); + put("teamcity", UsageFormatter.class); }}; // Refuse plugins known to implement the old API - private static final HashMap> OLD_INTELLIJ_IDEA_PLUGIN_CLASSES = new HashMap>() {{ - put("org.jetbrains.plugins.cucumber.java.run.CucumberJvmSMFormatter", PrettyFormatter.class); - put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm2SMFormatter", PrettyFormatter.class); - put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm3SMFormatter", PrettyFormatter.class); - put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm4SMFormatter", PrettyFormatter.class); - put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm5SMFormatter", PrettyFormatter.class); + private static final HashMap> INCOMPATIBLE_INTELLIJ_IDEA_PLUGIN_CLASSES = new HashMap>() {{ + put("org.jetbrains.plugins.cucumber.java.run.CucumberJvmSMFormatter", TeamCityPlugin.class); + put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm2SMFormatter", TeamCityPlugin.class); + put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm3SMFormatter", TeamCityPlugin.class); + put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm4SMFormatter", TeamCityPlugin.class); + put("org.jetbrains.plugins.cucumber.java.run.CucumberJvm5SMFormatter", TeamCityPlugin.class); }}; private final String pluginString; @@ -76,9 +78,9 @@ public static PluginOption parse(String pluginArgumentPattern) { } private static Class parsePluginName(String pluginName) { - Class oldApiPlugin = OLD_INTELLIJ_IDEA_PLUGIN_CLASSES.get(pluginName); + Class oldApiPlugin = INCOMPATIBLE_INTELLIJ_IDEA_PLUGIN_CLASSES.get(pluginName); if (oldApiPlugin != null) { - log.warn(() -> "Incompatible IntelliJ IDEA Plugin detected. Falling back to pretty formatter"); + log.debug(() -> "Incompatible IntelliJ IDEA Plugin detected. Falling back to teamcity plugin"); return oldApiPlugin; } diff --git a/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java b/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java new file mode 100644 index 0000000000..98b6d28e74 --- /dev/null +++ b/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java @@ -0,0 +1,379 @@ +package io.cucumber.core.plugin; + +import io.cucumber.core.feature.FeatureParser; +import io.cucumber.core.gherkin.Container; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.gherkin.Node; +import io.cucumber.plugin.EventListener; +import io.cucumber.plugin.event.EmbedEvent; +import io.cucumber.plugin.event.Event; +import io.cucumber.plugin.event.EventPublisher; +import io.cucumber.plugin.event.HookTestStep; +import io.cucumber.plugin.event.HookType; +import io.cucumber.plugin.event.PickleStepTestStep; +import io.cucumber.plugin.event.Result; +import io.cucumber.plugin.event.SnippetsSuggestedEvent; +import io.cucumber.plugin.event.Status; +import io.cucumber.plugin.event.TestCase; +import io.cucumber.plugin.event.TestCaseFinished; +import io.cucumber.plugin.event.TestCaseStarted; +import io.cucumber.plugin.event.TestRunFinished; +import io.cucumber.plugin.event.TestRunStarted; +import io.cucumber.plugin.event.TestSourceRead; +import io.cucumber.plugin.event.TestStep; +import io.cucumber.plugin.event.TestStepFinished; +import io.cucumber.plugin.event.TestStepStarted; +import io.cucumber.plugin.event.WriteEvent; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TeamCityPlugin implements EventListener { + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'hh:mm:ss.SSSZ"); + + private static final String TEAMCITY_PREFIX = "##teamcity"; + + private static final String TEMPLATE_ENTER_THE_MATRIX = TEAMCITY_PREFIX + "[enteredTheMatrix timestamp = '%s']"; + private static final String TEMPLATE_TEST_RUN_STARTED = TEAMCITY_PREFIX + "[testSuiteStarted timestamp = '%s' name = 'Cucumber']"; + private static final String TEMPLATE_TEST_RUN_FINISHED = TEAMCITY_PREFIX + "[testSuiteFinished timestamp = '%s' name = 'Cucumber']"; + + private static final String TEMPLATE_TEST_SUITE_STARTED = TEAMCITY_PREFIX + "[testSuiteStarted timestamp = '%s' locationHint = '%s' name = '%s']"; + private static final String TEMPLATE_TEST_SUITE_FINISHED = TEAMCITY_PREFIX + "[testSuiteFinished timestamp = '%s' name = '%s']"; + + private static final String TEMPLATE_TEST_STARTED = TEAMCITY_PREFIX + "[testStarted timestamp = '%s' locationHint = '%s' captureStandardOutput = 'true' name = '%s']"; + private static final String TEMPLATE_TEST_FINISHED = TEAMCITY_PREFIX + "[testFinished timestamp = '%s' duration = '%s' name = '%s']"; + private static final String TEMPLATE_TEST_FAILED = TEAMCITY_PREFIX + "[testFailed timestamp = '%s' duration = '%s' message = '%s' details = '%s' name = '%s']"; + private static final String TEMPLATE_TEST_IGNORED = TEAMCITY_PREFIX + "[testIgnored timestamp = '%s' duration = '%s' message = '%s' name = '%s']"; + + private static final String TEMPLATE_PROGRESS_COUNTING_STARTED = TEAMCITY_PREFIX + "[customProgressStatus testsCategory = 'Scenarios' count = '0' timestamp = '%s']"; + private static final String TEMPLATE_PROGRESS_COUNTING_FINISHED = TEAMCITY_PREFIX + "[customProgressStatus testsCategory = '' count = '0' timestamp = '%s']"; + private static final String TEMPLATE_PROGRESS_TEST_STARTED = TEAMCITY_PREFIX + "[customProgressStatus type = 'testStarted' timestamp = '%s']"; + private static final String TEMPLATE_PROGRESS_TEST_FINISHED = TEAMCITY_PREFIX + "[customProgressStatus type = 'testFinished' timestamp = '%s']"; + + private static final Pattern ANNOTATION_GLUE_CODE_LOCATION_PATTERN = Pattern.compile("^(.*)\\.(.*)\\(.*\\) in .*$"); + private static final Pattern LAMBDA_GLUE_CODE_LOCATION_PATTERN = Pattern.compile("^(.*):(.*)$"); + + private final PrintStream out; + private final List snippets = new ArrayList<>(); + private final Map features = new HashMap<>(); + private List currentStack = new ArrayList<>(); + + @SuppressWarnings("unused") // Used by PluginFactory + public TeamCityPlugin() { + // This plugin prints markers for Team City and IDEA that allows them + // associate the output to specific test cases. Printing to system out + // - and potentially mixing with other formatters - is intentional. + this(System.out); + } + + TeamCityPlugin(PrintStream out) { + this.out = out; + } + + @Override + public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(TestRunStarted.class, this::printTestRunStarted); + publisher.registerHandlerFor(TestCaseStarted.class, this::printTestCaseStarted); + publisher.registerHandlerFor(TestStepStarted.class, this::printTestStepStarted); + publisher.registerHandlerFor(TestStepFinished.class, this::printTestStepFinished); + publisher.registerHandlerFor(TestCaseFinished.class, this::printTestCaseFinished); + publisher.registerHandlerFor(TestRunFinished.class, this::printTestRunFinished); + publisher.registerHandlerFor(SnippetsSuggestedEvent.class, this::handleSnippetSuggested); + publisher.registerHandlerFor(EmbedEvent.class, this::handleEmbedEvent); + publisher.registerHandlerFor(WriteEvent.class, this::handleWriteEvent); + publisher.registerHandlerFor(TestSourceRead.class, this::handleTestSourceRead); + } + + private final FeatureParser featureParser = new FeatureParser(UUID::randomUUID); + + private void handleTestSourceRead(TestSourceRead event) { + features.put(event.getUri(), featureParser.parseResource(new TestSourceReadResource(event))); + } + + private void printTestRunStarted(TestRunStarted event) { + String timestamp = extractTimeStamp(event); + print(TEMPLATE_ENTER_THE_MATRIX, timestamp); + print(TEMPLATE_TEST_RUN_STARTED, timestamp); + print(TEMPLATE_PROGRESS_COUNTING_STARTED, timestamp); + } + + private String extractTimeStamp(Event event) { + ZonedDateTime date = event.getInstant().atZone(ZoneOffset.UTC); + return DATE_FORMAT.format(date); + } + + private void printTestCaseStarted(TestCaseStarted event) { + TestCase testCase = event.getTestCase(); + URI uri = testCase.getUri(); + Feature feature = features.get(uri); + String timestamp = extractTimeStamp(event); + + List newStack = extractStack(feature, testCase); + poppedNodes(newStack).forEach(node -> finishNode(timestamp, node)); + pushedNodes(newStack).forEach(node -> startNode(uri, timestamp, node)); + this.currentStack = newStack; + + print(TEMPLATE_PROGRESS_TEST_STARTED, timestamp); + } + + private void startNode(URI uri, String timestamp, Node node) { + String name = node.getName() == null ? node.getKeyWord() : node.getName(); + String location = uri + ":" + node.getLocation().getLine(); + print(TEMPLATE_TEST_SUITE_STARTED, timestamp, location, name); + } + + private void finishNode(String timestamp, Node node) { + String name = node.getName() == null ? node.getKeyWord() : node.getName(); + print(TEMPLATE_TEST_SUITE_FINISHED, timestamp, name); + } + + private List poppedNodes(List newStack) { + List nodes = new ArrayList<>(reversedPoppedNodes(currentStack, newStack)); + Collections.reverse(nodes); + return nodes; + } + + private List reversedPoppedNodes(List currentStack, List newStack) { + for (int i = 0; i < currentStack.size() && i < newStack.size(); i++) { + if (!currentStack.get(i).equals(newStack.get(i))) { + return currentStack.subList(i, currentStack.size()); + } + } + if (newStack.size() < currentStack.size()) { + return currentStack.subList(newStack.size(), currentStack.size()); + } + return Collections.emptyList(); + } + + private List pushedNodes(List newStack) { + for (int i = 0; i < currentStack.size() && i < newStack.size(); i++) { + if (!currentStack.get(i).equals(newStack.get(i))) { + return newStack.subList(i, newStack.size()); + } + } + if (newStack.size() < currentStack.size()) { + return Collections.emptyList(); + } + return newStack.subList(currentStack.size(), newStack.size()); + } + + private List extractStack(Feature feature, TestCase testCase) { + List stack = new ArrayList<>(); + findInFeature(stack, feature, testCase); + Collections.reverse(stack); + return stack; + } + + private boolean findInFeature(List stack, Node node, TestCase testCase) { + if (node.getLocation().getLine() == testCase.getLine()) { + stack.add(node); + return true; + } + + if (node instanceof Container) { + Container container = (Container) node; + for (Node child : container.children()) { + if (findInFeature(stack, child, testCase)) { + stack.add(node); + return true; + } + } + } + return false; + } + + private void printTestStepStarted(TestStepStarted event) { + String timestamp = extractTimeStamp(event); + String name = extractName(event.getTestStep()); + String location = extractLocation(event); + print(TEMPLATE_TEST_STARTED, timestamp, location, name); + } + + private String extractLocation(TestStepStarted event) { + TestStep testStep = event.getTestStep(); + if (testStep instanceof PickleStepTestStep) { + PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) testStep; + return pickleStepTestStep.getUri() + ":" + pickleStepTestStep.getStep().getLine(); + } + return extractSourceLocation(testStep); + } + + private String extractSourceLocation(TestStep testStep) { + + Matcher javaMatcher = ANNOTATION_GLUE_CODE_LOCATION_PATTERN.matcher(testStep.getCodeLocation()); + if (javaMatcher.matches()) { + String declaringClassName = javaMatcher.group(1); + String methodName = javaMatcher.group(2); + return String.format("java:test://%s/%s", declaringClassName, methodName); + } + Matcher java8Matcher = LAMBDA_GLUE_CODE_LOCATION_PATTERN.matcher(testStep.getCodeLocation()); + if (java8Matcher.matches()) { + String declaringClassName = java8Matcher.group(1); + String line = java8Matcher.group(2); + // TODO: Doesn't work with IDEA. What does? + return String.format("java:test://%s:%s", declaringClassName, line); + } + + return testStep.getCodeLocation(); + } + + private void printTestStepFinished(TestStepFinished event) { + String timeStamp = extractTimeStamp(event); + long duration = extractDuration(event.getResult()); + String name = extractName(event.getTestStep()); + + Throwable error = event.getResult().getError(); + Status status = event.getResult().getStatus(); + switch (status) { + case SKIPPED: + print(TEMPLATE_TEST_IGNORED, timeStamp, duration, error == null ? "Step skipped" : error.getMessage(), name); + break; + case PENDING: + print(TEMPLATE_TEST_IGNORED, timeStamp, duration, error == null ? "Step pending" : error.getMessage(), name); + break; + case UNDEFINED: + PickleStepTestStep testStep = (PickleStepTestStep) event.getTestStep(); + print(TEMPLATE_TEST_FAILED, timeStamp, duration, getSnippet(testStep), name); + break; + case AMBIGUOUS: + case FAILED: + String message = "Step failed"; + String details = extractStackTrace(error); + print(TEMPLATE_TEST_FAILED, timeStamp, duration, message, details, name); + break; + default: + break; + } + print(TEMPLATE_TEST_FINISHED, timeStamp, duration, name); + } + + private String extractStackTrace(Throwable error) { + ByteArrayOutputStream s = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(s); + error.printStackTrace(printStream); + return new String(s.toByteArray(), StandardCharsets.UTF_8); + } + + private String extractName(TestStep step) { + if (step instanceof PickleStepTestStep) { + PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) step; + return pickleStepTestStep.getStep().getText(); + } + if (step instanceof HookTestStep) { + HookTestStep hook = (HookTestStep) step; + HookType hookType = hook.getHookType(); + switch (hookType) { + case BEFORE: + return "Before"; + case AFTER: + return "After"; + case BEFORE_STEP: + return "BeforeStep"; + case AFTER_STEP: + return "AfterStep"; + default: + return hookType.name().toLowerCase(Locale.US); + } + } + return "Unknown step"; + } + + private String getSnippet(PickleStepTestStep testStep) { + StringBuilder builder = new StringBuilder("There was an undefined step\n"); + + if (snippets.isEmpty()) { + return builder.toString(); + } + + snippets.stream() + .filter(snippet -> + snippet.getStepLine() == testStep.getStep().getLine() && + snippet.getUri().equals(testStep.getUri()) + ) + .findFirst() + .ifPresent(event -> { + builder.append("You can implement missing steps with the snippets below:\n"); + event.getSnippets().forEach(snippet -> { + builder.append(snippet); + builder.append("\n"); + }); + } + ); + return builder.toString(); + } + + private void printTestCaseFinished(TestCaseFinished event) { + String timestamp = extractTimeStamp(event); + print(TEMPLATE_PROGRESS_TEST_FINISHED, timestamp); + finishNode(timestamp, currentStack.remove(currentStack.size() - 1)); + } + + private long extractDuration(Result result) { + return result.getDuration().toMillis(); + } + + private void printTestRunFinished(TestRunFinished event) { + String timestamp = extractTimeStamp(event); + print(TEMPLATE_PROGRESS_COUNTING_FINISHED, timestamp); + + List emptyStack = new ArrayList<>(); + poppedNodes(emptyStack).forEach(node -> finishNode(timestamp, node)); + currentStack = emptyStack; + + print(TEMPLATE_TEST_RUN_FINISHED, timestamp); + } + + private void handleSnippetSuggested(SnippetsSuggestedEvent event) { + snippets.add(event); + } + + private void handleEmbedEvent(EmbedEvent event) { + String name = event.getName() == null ? "" : event.getName() + " "; + print("Embed event: " + name + "[" + event.getMediaType() + " " + event.getData().length + " bytes]"); + } + + private void handleWriteEvent(WriteEvent event) { + print("Write event:\n" + event.getText()); + } + + private void print(String command, Object... args) { + out.println(formatCommand(command, args)); + } + + private String formatCommand(String command, Object... parameters) { + String[] escapedParameters = new String[parameters.length]; + for (int i = 0; i < escapedParameters.length; i++) { + escapedParameters[i] = escape(parameters[i].toString()); + } + + return String.format(command, escapedParameters); + } + + private String escape(String source) { + if (source == null) { + return ""; + } + return source + .replace("|", "||") + .replace("'", "|'") + .replace("\n", "|n") + .replace("\r", "|r") + .replace("[", "|[") + .replace("]", "|]"); + } + +} diff --git a/core/src/main/java/io/cucumber/core/runner/PickleStepDefinitionMatch.java b/core/src/main/java/io/cucumber/core/runner/PickleStepDefinitionMatch.java index 72a391b064..2944d56df6 100644 --- a/core/src/main/java/io/cucumber/core/runner/PickleStepDefinitionMatch.java +++ b/core/src/main/java/io/cucumber/core/runner/PickleStepDefinitionMatch.java @@ -157,7 +157,7 @@ public String getPattern() { } private StackTraceElement getStepLocation() { - return new StackTraceElement("✽", step.getText(), uri.getSchemeSpecificPart(), step.getLine()); + return new StackTraceElement("✽", step.getText(), uri.toString(), step.getLine()); } StepDefinition getStepDefinition() { diff --git a/core/src/main/java/io/cucumber/core/runner/Runner.java b/core/src/main/java/io/cucumber/core/runner/Runner.java index 0066fc5113..cfb996a431 100644 --- a/core/src/main/java/io/cucumber/core/runner/Runner.java +++ b/core/src/main/java/io/cucumber/core/runner/Runner.java @@ -119,7 +119,7 @@ private PickleStepDefinitionMatch matchStepToStepDefinition(Pickle pickle, Step } List snippets = generateSnippetsForStep(step); if (!snippets.isEmpty()) { - bus.send(new SnippetsSuggestedEvent(bus.getInstant(), pickle.getUri(), step.getLine(), snippets)); + bus.send(new SnippetsSuggestedEvent(bus.getInstant(), pickle.getUri(), pickle.getScenarioLocation().getLine(), step.getLine(), snippets)); } return new UndefinedPickleStepDefinitionMatch(pickle.getUri(), step); } catch (AmbiguousStepDefinitionsException e) { diff --git a/core/src/test/java/io/cucumber/core/plugin/CanonicalEventOrderTest.java b/core/src/test/java/io/cucumber/core/plugin/CanonicalEventOrderTest.java index 53b9d86697..61df2cf213 100644 --- a/core/src/test/java/io/cucumber/core/plugin/CanonicalEventOrderTest.java +++ b/core/src/test/java/io/cucumber/core/plugin/CanonicalEventOrderTest.java @@ -43,7 +43,7 @@ private static Event createTestCaseEvent(final URI uri, final int line) { private Event runStarted = new TestRunStarted(getInstant()); private Event testRead = new TestSourceRead(getInstant(), URI.create("file:path/to.feature"), "source"); - private Event suggested = new SnippetsSuggestedEvent(getInstant(), URI.create("file:path/to/1.feature"), 0, Collections.emptyList()); + private Event suggested = new SnippetsSuggestedEvent(getInstant(), URI.create("file:path/to/1.feature"), 0, 0, Collections.emptyList()); private Event feature1Case1Started = createTestCaseEvent(URI.create("file:path/to/1.feature"), 1); private Event feature1Case2Started = createTestCaseEvent(URI.create("file:path/to/1.feature"), 9); private Event feature1Case3Started = createTestCaseEvent(URI.create("file:path/to/1.feature"), 11); diff --git a/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java b/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java new file mode 100755 index 0000000000..616738203a --- /dev/null +++ b/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java @@ -0,0 +1,140 @@ +package io.cucumber.core.plugin; + +import io.cucumber.core.feature.TestFeatureParser; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.runner.TestHelper; +import io.cucumber.plugin.event.Result; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.cucumber.core.runner.TestHelper.createWriteHookAction; +import static io.cucumber.core.runner.TestHelper.result; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + +class TeamCityPluginTest { + + private final List features = new ArrayList<>(); + private final Map stepsToResult = new HashMap<>(); + private final Map stepsToLocation = new HashMap<>(); + private final List> hooks = new ArrayList<>(); + private final List hookLocations = new ArrayList<>(); + private final List> hookActions = new ArrayList<>(); + private final String location = new File("").toURI().toString(); + + @Test + void should_handle_scenario_outline() { + Feature feature = TestFeatureParser.parse("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario Outline: \n" + + " Given first step\n" + + " Then step\n" + + " Examples: examples name\n" + + " | name | arg |\n" + + " | name 1 | second |\n" + + " | name 2 | third |\n"); + features.add(feature); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + stepsToLocation.put("second step", "path/step_definitions.java:7"); + stepsToLocation.put("third step", "path/step_definitions.java:11"); + + String formatterOutput = runFeaturesWithFormatter(); + + assertThat(formatterOutput, containsString("" + + "##teamcity[enteredTheMatrix timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' name = 'Cucumber']\n" + + "##teamcity[customProgressStatus testsCategory = 'Scenarios' count = '0' timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:1' name = 'feature name']\n" + + "##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:2' name = '']\n" + + "##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:5' name = 'examples name']\n" + + "##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:7' name = 'Example #1']\n" + + "##teamcity[customProgressStatus type = 'testStarted' timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:3' captureStandardOutput = 'true' name = 'first step']\n" + + "##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' name = 'first step']\n" + + "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:4' captureStandardOutput = 'true' name = 'second step']\n" + + "##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' name = 'second step']\n" + + "##teamcity[customProgressStatus type = 'testFinished' timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Example #1']\n" + + "##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:8' name = 'Example #2']\n" + + "##teamcity[customProgressStatus type = 'testStarted' timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:3' captureStandardOutput = 'true' name = 'first step']\n" + + "##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' name = 'first step']\n" + + "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location + "path/test.feature:4' captureStandardOutput = 'true' name = 'third step']\n" + + "##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' name = 'third step']\n" + + "##teamcity[customProgressStatus type = 'testFinished' timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Example #2']\n" + + "##teamcity[customProgressStatus testsCategory = '' count = '0' timestamp = '1970-01-01T12:00:00.000+0000']\n" + + "##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'examples name']\n" + + "##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = '']\n" + + "##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'feature name']\n" + + "##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Cucumber']\n" + )); + } + + @Test + void should_print_error_message_for_failed_steps() { + Feature feature = TestFeatureParser.parse("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + features.add(feature); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + stepsToResult.put("first step", result("failed")); + + String formatterOutput = runFeaturesWithFormatter(); + + assertThat(formatterOutput, containsString("" + + "##teamcity[testFailed timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' message = 'Step failed' details = 'the stack trace' name = 'first step']\n" + )); + } + + @Test + void should_print_error_message_for_before_hooks() { + Feature feature = TestFeatureParser.parse("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + features.add(feature); + stepsToLocation.put("first step", "path/step_definitions.java:3"); + stepsToResult.put("first step", result("passed")); + hooks.add(TestHelper.hookEntry("before", result("failed"))); + hookLocations.add("HookDefinition.java:3"); + + String formatterOutput = runFeaturesWithFormatter(); + + assertThat(formatterOutput, containsString("" + + "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://HookDefinition.java:3' captureStandardOutput = 'true' name = 'Before']\n" + + "##teamcity[testFailed timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' message = 'Step failed' details = 'the stack trace' name = 'Before']\n" + )); + } + + private String runFeaturesWithFormatter() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(byteArrayOutputStream); + final TeamCityPlugin formatter = new TeamCityPlugin(printStream); + + TestHelper.builder() + .withFormatterUnderTest(formatter) + .withFeatures(features) + .withStepsToResult(stepsToResult) + .withStepsToLocation(stepsToLocation) + .withHooks(hooks) + .withHookLocations(hookLocations) + .withHookActions(hookActions) + .build() + .run(); + + return new String(byteArrayOutputStream.toByteArray(), UTF_8); + } + +} diff --git a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java index 04ed08b2c4..d649638259 100644 --- a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java +++ b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java @@ -4,22 +4,28 @@ import io.cucumber.core.gherkin.Examples; import io.cucumber.core.gherkin.Location; +import java.util.Collection; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; +import java.util.stream.Collectors; final class GherkinMessagesExamples implements Examples { private final io.cucumber.messages.Messages.GherkinDocument.Feature.Scenario.Examples examples; + private final List children; GherkinMessagesExamples(io.cucumber.messages.Messages.GherkinDocument.Feature.Scenario.Examples examples) { this.examples = examples; + + AtomicInteger row = new AtomicInteger(1); + this.children = examples.getTableBodyList().stream() + .map(tableRow -> new GherkinMessagesExample(tableRow, row.getAndIncrement())) + .collect(Collectors.toList()); } @Override - public Stream children() { - AtomicInteger row = new AtomicInteger(1); - return examples.getTableBodyList().stream() - .map(tableRow -> new GherkinMessagesExample(tableRow, row.getAndIncrement())); + public Collection children() { + return children; } @Override diff --git a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesFeature.java b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesFeature.java index 89768684d7..3b03400023 100644 --- a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesFeature.java +++ b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesFeature.java @@ -3,17 +3,18 @@ import io.cucumber.core.gherkin.Feature; import io.cucumber.core.gherkin.Located; import io.cucumber.core.gherkin.Location; -import io.cucumber.core.gherkin.Pickle; import io.cucumber.core.gherkin.Node; +import io.cucumber.core.gherkin.Pickle; import io.cucumber.messages.Messages; import io.cucumber.messages.Messages.GherkinDocument; import io.cucumber.messages.Messages.GherkinDocument.Feature.Scenario; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Stream; +import java.util.stream.Collectors; final class GherkinMessagesFeature implements Feature { private final URI uri; @@ -21,6 +22,7 @@ final class GherkinMessagesFeature implements Feature { private final List envelopes; private final GherkinDocument gherkinDocument; private final String gherkinSource; + private final List children; GherkinMessagesFeature(GherkinDocument gherkinDocument, URI uri, String gherkinSource, List pickles, List envelopes) { this.gherkinDocument = gherkinDocument; @@ -28,24 +30,25 @@ final class GherkinMessagesFeature implements Feature { this.gherkinSource = gherkinSource; this.pickles = pickles; this.envelopes = envelopes; - } - - @Override - public Stream children() { - return gherkinDocument.getFeature().getChildrenList().stream() + this.children = gherkinDocument.getFeature().getChildrenList().stream() .filter(featureChild -> featureChild.hasRule() || featureChild.hasScenario()) .map(featureChild -> { if (featureChild.hasRule()) { return new GherkinMessagesRule(featureChild.getRule()); } - Scenario scenario = featureChild.getScenario(); if (scenario.getExamplesCount() > 0) { return new GherkinMessagesScenarioOutline(scenario); } else { return new GherkinMessagesScenario(scenario); } - }); + }) + .collect(Collectors.toList()); + } + + @Override + public Collection children() { + return children; } @Override diff --git a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java index 60d781d69e..659060eba2 100644 --- a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java +++ b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java @@ -1,24 +1,23 @@ package io.cucumber.core.gherkin.messages; import io.cucumber.core.gherkin.Location; -import io.cucumber.core.gherkin.Rule; import io.cucumber.core.gherkin.Node; +import io.cucumber.core.gherkin.Rule; import io.cucumber.messages.Messages; import io.cucumber.messages.Messages.GherkinDocument.Feature.FeatureChild.RuleChild; -import java.util.stream.Stream; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; final class GherkinMessagesRule implements Rule { private final Messages.GherkinDocument.Feature.FeatureChild.Rule rule; + private final List children; GherkinMessagesRule(Messages.GherkinDocument.Feature.FeatureChild.Rule rule) { this.rule = rule; - } - - @Override - public Stream children() { - return rule.getChildrenList().stream() + this.children = rule.getChildrenList().stream() .filter(RuleChild::hasScenario) .map(ruleChild -> { Messages.GherkinDocument.Feature.Scenario scenario = ruleChild.getScenario(); @@ -27,7 +26,13 @@ public Stream children() { } else { return new GherkinMessagesScenario(scenario); } - }); + }) + .collect(Collectors.toList()); + } + + @Override + public Collection children() { + return children; } @Override diff --git a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java index 404f07d1db..da9aec6286 100644 --- a/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java +++ b/gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java @@ -5,21 +5,26 @@ import io.cucumber.core.gherkin.ScenarioOutline; import io.cucumber.messages.Messages.GherkinDocument.Feature.Scenario; -import java.util.stream.Stream; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; final class GherkinMessagesScenarioOutline implements ScenarioOutline { private final Scenario scenario; + private final List children; GherkinMessagesScenarioOutline(Scenario scenario) { this.scenario = scenario; + this.children = scenario.getExamplesList().stream() + .map(GherkinMessagesExamples::new) + .collect(Collectors.toList()); } @Override - public Stream children() { - return scenario.getExamplesList().stream() - .map(GherkinMessagesExamples::new); + public Collection children() { + return children; } @Override diff --git a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageExamples.java b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageExamples.java index c3555b88b6..dc1284d7da 100644 --- a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageExamples.java +++ b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageExamples.java @@ -4,26 +4,32 @@ import io.cucumber.core.gherkin.Examples; import io.cucumber.core.gherkin.Location; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; +import java.util.stream.Collectors; final class GherkinVintageExamples implements Examples { + private final List children; private final gherkin.ast.Examples examples; GherkinVintageExamples(gherkin.ast.Examples examples) { this.examples = examples; - } - - @Override - public Stream children() { if (examples.getTableBody() == null) { - return Stream.empty(); + this.children = Collections.emptyList(); + } else { + AtomicInteger rowCounter = new AtomicInteger(1); + this.children = examples.getTableBody().stream() + .map(tableRow -> new GherkinVintageExample(tableRow, rowCounter.getAndIncrement())) + .collect(Collectors.toList()); } + } - AtomicInteger rowCounter = new AtomicInteger(1); - return examples.getTableBody().stream() - .map(tableRow -> new GherkinVintageExample(tableRow, rowCounter.getAndIncrement())); + @Override + public Collection children() { + return children; } @Override diff --git a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageFeature.java b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageFeature.java index c02a9dfd93..90bed79f3b 100644 --- a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageFeature.java +++ b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageFeature.java @@ -9,9 +9,11 @@ import io.cucumber.core.gherkin.Pickle; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; final class GherkinVintageFeature implements Feature { @@ -19,24 +21,27 @@ final class GherkinVintageFeature implements Feature { private final List pickles; private final GherkinDocument gherkinDocument; private final String gherkinSource; + private final List children; GherkinVintageFeature(GherkinDocument gherkinDocument, URI uri, String gherkinSource, List pickles) { this.gherkinDocument = gherkinDocument; this.uri = uri; this.gherkinSource = gherkinSource; this.pickles = pickles; - } - - @Override - public Stream children() { - return gherkinDocument.getFeature().getChildren().stream() + this.children = gherkinDocument.getFeature().getChildren().stream() .map(scenarioDefinition -> { if (scenarioDefinition instanceof ScenarioOutline) { ScenarioOutline outline = (ScenarioOutline) scenarioDefinition; return new GherkinVintageScenarioOutline(outline); } return new GherkinVintageScenario(scenarioDefinition); - }).map(Node.class::cast); + }).map(Node.class::cast) + .collect(Collectors.toList()); + } + + @Override + public Collection children() { + return children; } @Override diff --git a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintagePickle.java b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintagePickle.java index ef901b0d0e..c5c77858bd 100644 --- a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintagePickle.java +++ b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintagePickle.java @@ -13,6 +13,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static io.cucumber.core.gherkin.vintage.GherkinVintageLocation.from; @@ -28,6 +29,7 @@ final class GherkinVintagePickle implements Pickle { private final List steps; private final URI uri; private final String keyWord; + private final String id; GherkinVintagePickle(gherkin.pickles.Pickle pickle, URI uri, GherkinDocument document, GherkinDialect dialect) { this.pickle = pickle; @@ -38,6 +40,11 @@ final class GherkinVintagePickle implements Pickle { .map(ScenarioDefinition::getKeyword) .findFirst() .orElse("Scenario"); + this.id = pickle.getName() + ":" + pickle.getLocations() + .stream() + .map(l -> String.valueOf(l.getLine())) + .collect(Collectors.joining(":")); + } private static List createCucumberSteps(gherkin.pickles.Pickle pickle, GherkinDocument document, GherkinDialect dialect, String uri) { @@ -104,11 +111,19 @@ public URI getUri() { @Override public String getId() { - return pickle.getName() + ":" + pickle.getLocations() - .stream() - .map(l -> String.valueOf(l.getLine())) - .collect(Collectors.joining(":")); + return id; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GherkinVintagePickle that = (GherkinVintagePickle) o; + return id.equals(that.id); + } + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageScenarioOutline.java b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageScenarioOutline.java index 8fa8bb5ff7..de35b5ef14 100644 --- a/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageScenarioOutline.java +++ b/gherkin-vintage/src/main/java/io/cucumber/core/gherkin/vintage/GherkinVintageScenarioOutline.java @@ -4,22 +4,27 @@ import io.cucumber.core.gherkin.Location; import io.cucumber.core.gherkin.ScenarioOutline; -import java.util.stream.Stream; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; import static io.cucumber.core.gherkin.vintage.GherkinVintageLocation.from; final class GherkinVintageScenarioOutline implements ScenarioOutline { private final gherkin.ast.ScenarioOutline scenarioOutline; + private final List children; GherkinVintageScenarioOutline(gherkin.ast.ScenarioOutline scenarioOutline) { this.scenarioOutline = scenarioOutline; + this.children = scenarioOutline.getExamples().stream() + .map(GherkinVintageExamples::new) + .collect(Collectors.toList()); } @Override - public Stream children() { - return scenarioOutline.getExamples().stream() - .map(GherkinVintageExamples::new); + public Collection children() { + return children; } @Override diff --git a/gherkin/src/main/java/io/cucumber/core/gherkin/Container.java b/gherkin/src/main/java/io/cucumber/core/gherkin/Container.java index 147cd0f57e..2bc8e8880f 100644 --- a/gherkin/src/main/java/io/cucumber/core/gherkin/Container.java +++ b/gherkin/src/main/java/io/cucumber/core/gherkin/Container.java @@ -1,8 +1,8 @@ package io.cucumber.core.gherkin; -import java.util.stream.Stream; +import java.util.Collection; -public interface Container { +public interface Container { - Stream children(); + Collection children(); } diff --git a/java8/src/main/java/io/cucumber/java8/AbstractGlueDefinition.java b/java8/src/main/java/io/cucumber/java8/AbstractGlueDefinition.java index a37ad8a6a1..577eef656d 100644 --- a/java8/src/main/java/io/cucumber/java8/AbstractGlueDefinition.java +++ b/java8/src/main/java/io/cucumber/java8/AbstractGlueDefinition.java @@ -22,7 +22,7 @@ abstract class AbstractGlueDefinition implements ScenarioScoped { } public final String getLocation() { - return location.getFileName() + ":" + location.getLineNumber(); + return location.getClassName() + ":" + location.getLineNumber(); } public final boolean isDefinedAt(StackTraceElement stackTraceElement) { diff --git a/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/TestCaseResultObserverTest.java b/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/TestCaseResultObserverTest.java index 30316b80ff..038f3c1307 100644 --- a/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/TestCaseResultObserverTest.java +++ b/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/TestCaseResultObserverTest.java @@ -196,7 +196,7 @@ void skippedByUser() { void undefined() { bus.send(new TestCaseStarted(Instant.now(), testCase)); bus.send(new TestStepStarted(Instant.now(), testCase, testStep)); - bus.send(new SnippetsSuggestedEvent(Instant.now(), uri, testStep.getStepLine(), asList( + bus.send(new SnippetsSuggestedEvent(Instant.now(), uri, testCase.getLine(), testStep.getStepLine(), asList( "mocked snippet 1", "mocked snippet 2", "mocked snippet 3" diff --git a/junit/src/test/java/io/cucumber/junit/JUnitReporterWithStepNotificationsTest.java b/junit/src/test/java/io/cucumber/junit/JUnitReporterWithStepNotificationsTest.java index 85f6d711cd..b9d7b584c4 100644 --- a/junit/src/test/java/io/cucumber/junit/JUnitReporterWithStepNotificationsTest.java +++ b/junit/src/test/java/io/cucumber/junit/JUnitReporterWithStepNotificationsTest.java @@ -228,7 +228,7 @@ void test_step_finished_fires_assumption_failed_and_test_finished_for_skipped_st void test_step_undefined_fires_assumption_failed_and_test_finished_for_undefined_step() { jUnitReporter.startExecutionUnit(pickleRunner, runNotifier); - bus.send(new SnippetsSuggestedEvent(now(), featureUri, scenarioLine, singletonList("some snippet"))); + bus.send(new SnippetsSuggestedEvent(now(), featureUri, scenarioLine, scenarioLine, singletonList("some snippet"))); bus.send(new TestCaseStarted(now(), testCase)); bus.send(new TestStepStarted(now(), testCase, mockTestStep(step))); Throwable exception = new CucumberException("No step definitions found"); @@ -266,7 +266,7 @@ void test_step_undefined_fires_test_failure_and_test_finished_for_undefined_step jUnitReporter.startExecutionUnit(pickleRunner, runNotifier); - bus.send(new SnippetsSuggestedEvent(now(), featureUri, scenarioLine, singletonList("some snippet"))); + bus.send(new SnippetsSuggestedEvent(now(), featureUri, scenarioLine, scenarioLine, singletonList("some snippet"))); bus.send(new TestCaseStarted(now(), testCase)); bus.send(new TestStepStarted(now(), testCase, mockTestStep(step))); Throwable exception = new CucumberException("No step definitions found"); diff --git a/plugin/src/main/java/io/cucumber/plugin/event/SnippetsSuggestedEvent.java b/plugin/src/main/java/io/cucumber/plugin/event/SnippetsSuggestedEvent.java index bea7cc0d1b..4ea8673650 100644 --- a/plugin/src/main/java/io/cucumber/plugin/event/SnippetsSuggestedEvent.java +++ b/plugin/src/main/java/io/cucumber/plugin/event/SnippetsSuggestedEvent.java @@ -12,12 +12,14 @@ @API(status = API.Status.STABLE) public final class SnippetsSuggestedEvent extends TimeStampedEvent { private final URI uri; + private final int scenarioLine; private final int stepLine; private final List snippets; - public SnippetsSuggestedEvent(Instant timeInstant, URI uri, int stepLine, List snippets) { + public SnippetsSuggestedEvent(Instant timeInstant, URI uri, int scenarioLine, int stepLine, List snippets) { super(timeInstant); this.uri = requireNonNull(uri); + this.scenarioLine = scenarioLine; this.stepLine = stepLine; this.snippets = unmodifiableList(requireNonNull(snippets)); } diff --git a/testng/src/test/java/io/cucumber/testng/TestCaseResultListenerTest.java b/testng/src/test/java/io/cucumber/testng/TestCaseResultListenerTest.java index e3bb7e3913..38e908d1f9 100644 --- a/testng/src/test/java/io/cucumber/testng/TestCaseResultListenerTest.java +++ b/testng/src/test/java/io/cucumber/testng/TestCaseResultListenerTest.java @@ -99,7 +99,7 @@ public void should_not_be_passed_for_ambiguous_result() { public void should_be_skipped_for_undefined_result() { TestCaseResultListener resultListener = new TestCaseResultListener(bus, false); - bus.send(new SnippetsSuggestedEvent(now(), uri, line, singletonList("stub snippet"))); + bus.send(new SnippetsSuggestedEvent(now(), uri, line, line, singletonList("stub snippet"))); Result stepResult = new Result(UNDEFINED, ZERO, error); bus.send(new TestStepFinished(now(), testCase, step, stepResult)); @@ -122,7 +122,7 @@ public void should_be_skipped_for_undefined_result() { public void should_not_be_skipped_for_undefined_result_in_strict_mode() { TestCaseResultListener resultListener = new TestCaseResultListener(bus, true); - bus.send(new SnippetsSuggestedEvent(now(), uri, line, singletonList("stub snippet"))); + bus.send(new SnippetsSuggestedEvent(now(), uri, line, line, singletonList("stub snippet"))); Result stepResult = new Result(UNDEFINED, ZERO, error); bus.send(new TestStepFinished(now(), testCase, step, stepResult));