diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index 5625e531fe..7d19b4bbe8 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -18,8 +18,8 @@ <gherkin.version>28.0.0</gherkin.version> <html-formatter.version>21.3.1</html-formatter.version> <junit-xml-formatter.version>0.4.0</junit-xml-formatter.version> - <messages.version>24.1.0</messages.version> - <query.version>12.1.2</query.version> + <messages.version>24.1.1-SNAPSHOT</messages.version> + <query.version>12.1.3-SNAPSHOT</query.version> <tag-expressions.version>6.1.0</tag-expressions.version> <testng-xml-formatter.version>0.1.0</testng-xml-formatter.version> </properties> diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/CucumberJvmJson.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/CucumberJvmJson.java new file mode 100644 index 0000000000..0f1504ddeb --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/CucumberJvmJson.java @@ -0,0 +1,394 @@ +package io.cucumber.core.plugin; + +import java.util.List; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * Object representation of <a href="https://github.com/cucumber/cucumber-json-schema/blob/main/schemas/cucumber-jvm.json">cucumber-jvm.json</a> schema. + */ +class CucumberJvmJson { + enum JvmElementType { + background, scenario + } + enum JvmStatus { + passed, + failed, + skipped, + undefined, + pending + } + + static class JvmFeature { + private final String uri; + private final String id; + private final Long line; + private final String keyword; + private final String name; + private final String description; + private final List<JvmElement> elements; + private final List<JvmLocationTag> tags; + + JvmFeature(String uri, String id, Long line, String keyword, String name, String description, List<JvmElement> elements, List<JvmLocationTag> tags) { + this.uri = requireNonNull(uri); + this.id = requireNonNull(id); + this.line = requireNonNull(line); + this.keyword = requireNonNull(keyword); + this.name = requireNonNull(name); + this.description = requireNonNull(description); + this.elements = requireNonNull(elements); + this.tags = tags; + } + + public String getUri() { + return uri; + } + + public String getId() { + return id; + } + + public Long getLine() { + return line; + } + + public String getKeyword() { + return keyword; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public List<JvmElement> getElements() { + return elements; + } + + public List<JvmLocationTag> getTags() { + return tags; + } + } + + static class JvmElement { + private final String start_timestamp; + private final Long line; + private final String id; + private final JvmElementType type; + private final String keyword; + private final String name; + private final String description; + private final List<JvmStep> steps; + private final List<JvmHook> before; + private final List<JvmHook> after; + private final List<JvmTag> tags; + + JvmElement(String start_timestamp, Long line, String id, JvmElementType type, String keyword, String name, String description, List<JvmStep> steps, List<JvmHook> before, List<JvmHook> after, List<JvmTag> tags) { + this.start_timestamp = start_timestamp; + this.line = requireNonNull(line); + this.id = id; + this.type = requireNonNull(type); + this.keyword = requireNonNull(keyword); + this.name = requireNonNull(name); + this.description = requireNonNull(description); + this.steps = requireNonNull(steps); + this.before = before; + this.after = after; + this.tags = tags; + } + + public String getStart_timestamp() { + return start_timestamp; + } + + public Long getLine() { + return line; + } + + public String getId() { + return id; + } + + public JvmElementType getType() { + return type; + } + + public String getKeyword() { + return keyword; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public List<JvmStep> getSteps() { + return steps; + } + + public List<JvmHook> getBefore() { + return before; + } + + public List<JvmHook> getAfter() { + return after; + } + + public List<JvmTag> getTags() { + return tags; + } + } + + static class JvmStep { + private final String keyword; + private final Long line; + private final JvmMatch match; + private final String name; + private final JvmResult result; + private final JvmDocString doc_string; + private final List<JvmDataTableRow> rows; + + JvmStep(String keyword, Long line, JvmMatch match, String name, JvmResult result, JvmDocString doc_string, List<JvmDataTableRow> rows) { + this.keyword = requireNonNull(keyword); + this.line = requireNonNull(line); + this.match = match; + this.name = requireNonNull(name); + this.result = requireNonNull(result); + this.doc_string = doc_string; + this.rows = rows; + } + + public String getKeyword() { + return keyword; + } + + public Long getLine() { + return line; + } + + public JvmMatch getMatch() { + return match; + } + + public String getName() { + return name; + } + + public JvmResult getResult() { + return result; + } + + public JvmDocString getDoc_string() { + return doc_string; + } + + public List<JvmDataTableRow> getRows() { + return rows; + } + } + + static class JvmMatch { + private final String location; + private final List<JvmArgument> arguments; + + JvmMatch(String location, List<JvmArgument> arguments) { + this.location = location; + this.arguments = arguments; + } + + public String getLocation() { + return location; + } + + public List<JvmArgument> getArguments() { + return arguments; + } + } + + static class JvmArgument { + private final String val; + private final Number offset; + + JvmArgument(String val, Number offset) { + this.val = requireNonNull(val); + this.offset = requireNonNull(offset); + } + + public String getVal() { + return val; + } + + public Number getOffset() { + return offset; + } + } + + static class JvmResult { + private final Long duration; + private final JvmStatus status; + private final String error_message; + + JvmResult(Long duration, JvmStatus status, String error_message) { + this.duration = duration; + this.status = requireNonNull(status); + this.error_message = error_message; + } + + public Long getDuration() { + return duration; + } + + public JvmStatus getStatus() { + return status; + } + + public String getError_message() { + return error_message; + } + } + + static class JvmDocString { + private final Long line; + private final String value; + private final String content_type; + + JvmDocString(Long line, String value, String content_type) { + this.line = requireNonNull(line); + this.value = requireNonNull(value); + this.content_type = content_type; + } + + public Long getLine() { + return line; + } + + public String getValue() { + return value; + } + + public String getContent_type() { + return content_type; + } + } + + static class JvmDataTableRow { + private final List<String> cells; + + JvmDataTableRow(List<String> cells) { + this.cells = requireNonNull(cells); + } + + public List<String> getCells() { + return cells; + } + } + + static class JvmHook { + private final JvmMatch match; + private final JvmResult result; + private final List<JvmEmbedding> embeddings; + + JvmHook(JvmMatch match, JvmResult result, List<JvmEmbedding> embeddings) { + this.match = requireNonNull(match); + this.result = requireNonNull(result); + this.embeddings = embeddings; + } + + public JvmMatch getMatch() { + return match; + } + + public JvmResult getResult() { + return result; + } + + public List<JvmEmbedding> getEmbeddings() { + return embeddings; + } + } + + static class JvmEmbedding { + private final String mime_type; + private final String data; + private final String name; + + JvmEmbedding(String mime_type, String data, String name) { + this.mime_type = requireNonNull(mime_type); + this.data = requireNonNull(data); + this.name = name; + } + + public String getData() { + return data; + } + + public String getMime_type() { + return mime_type; + } + + public String getName() { + return name; + } + } + + static class JvmTag { + private final String name; + + JvmTag(String name) { + this.name = requireNonNull(name); + } + + public String getName() { + return name; + } + } + + static class JvmLocationTag { + private final String name; + private final String type; + private final JvmLocation location; + + JvmLocationTag(String name, String type, JvmLocation location) { + this.name = requireNonNull(name); + this.type = requireNonNull(type); + this.location = requireNonNull(location); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public JvmLocation getLocation() { + return location; + } + } + + static class JvmLocation { + private final Long line; + private final Long column; + + JvmLocation(Long line, Long column) { + this.line = requireNonNull(line); + this.column = requireNonNull(column); + } + + public Long getLine() { + return line; + } + + public Long getColumn() { + return column; + } + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/IdNamingVisitor.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/IdNamingVisitor.java new file mode 100644 index 0000000000..00e8f81f05 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/IdNamingVisitor.java @@ -0,0 +1,42 @@ +package io.cucumber.core.plugin; + +import io.cucumber.messages.types.Examples; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.Pickle; +import io.cucumber.messages.types.Rule; +import io.cucumber.messages.types.Scenario; +import io.cucumber.query.NamingStrategy; + +import static io.cucumber.core.plugin.TestSourcesModel.convertToId; + +class IdNamingVisitor implements NamingStrategy.NamingVisitor { + @Override + public String accept(Feature feature) { + return convertToId(feature.getName()); + } + + @Override + public String accept(Rule rule) { + return convertToId(rule.getName()); + } + + @Override + public String accept(Scenario scenario) { + return convertToId(scenario.getName()); + } + + @Override + public String accept(Examples examples) { + return convertToId(examples.getName()); + } + + @Override + public String accept(int examplesIndex, int exampleIndex) { + return (examplesIndex + 1) + ";" + (examplesIndex + 1); + } + + @Override + public String accept(Pickle pickle) { + return convertToId(pickle.getName()); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java index f36526e4ea..922929b0a6 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java @@ -1,442 +1,41 @@ package io.cucumber.core.plugin; -import io.cucumber.messages.types.Background; -import io.cucumber.messages.types.Feature; -import io.cucumber.messages.types.Scenario; -import io.cucumber.messages.types.Step; +import io.cucumber.messages.types.Envelope; +import io.cucumber.plugin.ConcurrentEventListener; import io.cucumber.plugin.EventListener; -import io.cucumber.plugin.event.Argument; -import io.cucumber.plugin.event.DataTableArgument; -import io.cucumber.plugin.event.DocStringArgument; -import io.cucumber.plugin.event.EmbedEvent; 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.Status; -import io.cucumber.plugin.event.StepArgument; -import io.cucumber.plugin.event.TestCase; -import io.cucumber.plugin.event.TestCaseStarted; -import io.cucumber.plugin.event.TestRunFinished; -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.IOException; import java.io.OutputStream; -import java.io.Writer; -import java.net.URI; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import static io.cucumber.core.exception.ExceptionUtils.printStackTrace; -import static io.cucumber.core.plugin.TestSourcesModel.getBackgroundForTestCase; -import static java.util.Collections.singletonList; -import static java.util.Locale.ROOT; -import static java.util.stream.Collectors.toList; +public final class JsonFormatter implements ConcurrentEventListener { -public final class JsonFormatter implements EventListener { + private final MessagesToJsonWriter writer; - private static final String before = "before"; - private static final String after = "after"; - private final List<Map<String, Object>> featureMaps = new ArrayList<>(); - private final Map<String, Object> currentBeforeStepHookList = new HashMap<>(); - private final Writer writer; - private final TestSourcesModel testSources = new TestSourcesModel(); - private URI currentFeatureFile; - private List<Map<String, Object>> currentElementsList; - private Map<String, Object> currentElementMap; - private Map<String, Object> currentTestCaseMap; - private List<Map<String, Object>> currentStepsList; - private Map<String, Object> currentStepOrHookMap; - - @SuppressWarnings("WeakerAccess") // Used by PluginFactory public JsonFormatter(OutputStream out) { - this.writer = new UTF8OutputStreamWriter(out); + this.writer = new MessagesToJsonWriter(out, Jackson.OBJECT_MAPPER::writeValue); } @Override public void setEventPublisher(EventPublisher publisher) { - publisher.registerHandlerFor(TestSourceRead.class, this::handleTestSourceRead); - publisher.registerHandlerFor(TestCaseStarted.class, this::handleTestCaseStarted); - publisher.registerHandlerFor(TestStepStarted.class, this::handleTestStepStarted); - publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished); - publisher.registerHandlerFor(WriteEvent.class, this::handleWrite); - publisher.registerHandlerFor(EmbedEvent.class, this::handleEmbed); - publisher.registerHandlerFor(TestRunFinished.class, this::finishReport); - } - - private void handleTestSourceRead(TestSourceRead event) { - testSources.addTestSourceReadEvent(event.getUri(), event); - } - - @SuppressWarnings("unchecked") - private void handleTestCaseStarted(TestCaseStarted event) { - if (currentFeatureFile == null || !currentFeatureFile.equals(event.getTestCase().getUri())) { - currentFeatureFile = event.getTestCase().getUri(); - Map<String, Object> currentFeatureMap = createFeatureMap(event.getTestCase()); - featureMaps.add(currentFeatureMap); - currentElementsList = (List<Map<String, Object>>) currentFeatureMap.get("elements"); - } - currentTestCaseMap = createTestCase(event); - if (testSources.hasBackground(currentFeatureFile, event.getTestCase().getLocation().getLine())) { - currentElementMap = createBackground(event.getTestCase()); - currentElementsList.add(currentElementMap); - } else { - currentElementMap = currentTestCaseMap; - } - currentElementsList.add(currentTestCaseMap); - currentStepsList = (List<Map<String, Object>>) currentElementMap.get("steps"); - } - - @SuppressWarnings("unchecked") - private void handleTestStepStarted(TestStepStarted event) { - if (event.getTestStep() instanceof PickleStepTestStep) { - PickleStepTestStep testStep = (PickleStepTestStep) event.getTestStep(); - if (isFirstStepAfterBackground(testStep)) { - currentElementMap = currentTestCaseMap; - currentStepsList = (List<Map<String, Object>>) currentElementMap.get("steps"); - } - currentStepOrHookMap = createTestStep(testStep); - // add beforeSteps list to current step - if (currentBeforeStepHookList.containsKey(before)) { - currentStepOrHookMap.put(before, currentBeforeStepHookList.get(before)); - currentBeforeStepHookList.clear(); - } - currentStepsList.add(currentStepOrHookMap); - } else if (event.getTestStep() instanceof HookTestStep) { - HookTestStep hookTestStep = (HookTestStep) event.getTestStep(); - currentStepOrHookMap = createHookStep(hookTestStep); - addHookStepToTestCaseMap(currentStepOrHookMap, hookTestStep.getHookType()); - } else { - throw new IllegalStateException(); - } - } - - private void handleTestStepFinished(TestStepFinished event) { - currentStepOrHookMap.put("match", createMatchMap(event.getTestStep(), event.getResult())); - currentStepOrHookMap.put("result", createResultMap(event.getResult())); + publisher.registerHandlerFor(Envelope.class, this::write); } - private void handleWrite(WriteEvent event) { - addOutputToHookMap(event.getText()); - } - - private void handleEmbed(EmbedEvent event) { - addEmbeddingToHookMap(event.getData(), event.getMediaType(), event.getName()); - } - - private void finishReport(TestRunFinished event) { - Throwable exception = event.getResult().getError(); - if (exception != null) { - featureMaps.add(createDummyFeatureForFailure(event)); - } - + private void write(Envelope event) { try { - Jackson.OBJECT_MAPPER.writeValue(writer, featureMaps); - writer.close(); + writer.write(event); } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private Map<String, Object> createFeatureMap(TestCase testCase) { - Map<String, Object> featureMap = new HashMap<>(); - featureMap.put("uri", TestSourcesModel.relativize(testCase.getUri())); - featureMap.put("elements", new ArrayList<Map<String, Object>>()); - Feature feature = testSources.getFeature(testCase.getUri()); - if (feature != null) { - featureMap.put("keyword", feature.getKeyword()); - featureMap.put("name", feature.getName()); - featureMap.put("description", feature.getDescription() != null ? feature.getDescription() : ""); - featureMap.put("line", feature.getLocation().getLine()); - featureMap.put("id", TestSourcesModel.convertToId(feature.getName())); - featureMap.put("tags", feature.getTags().stream().map( - tag -> { - Map<String, Object> json = new LinkedHashMap<>(); - json.put("name", tag.getName()); - json.put("type", "Tag"); - Map<String, Object> location = new LinkedHashMap<>(); - location.put("line", tag.getLocation().getLine()); - location.put("column", tag.getLocation().getColumn()); - json.put("location", location); - return json; - }).collect(toList())); - - } - return featureMap; - } - - private Map<String, Object> createTestCase(TestCaseStarted event) { - Map<String, Object> testCaseMap = new HashMap<>(); - - testCaseMap.put("start_timestamp", getDateTimeFromTimeStamp(event.getInstant())); - - TestCase testCase = event.getTestCase(); - - testCaseMap.put("name", testCase.getName()); - testCaseMap.put("line", testCase.getLine()); - testCaseMap.put("type", "scenario"); - TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLine()); - if (astNode != null) { - testCaseMap.put("id", TestSourcesModel.calculateId(astNode)); - Scenario scenarioDefinition = TestSourcesModel.getScenarioDefinition(astNode); - testCaseMap.put("keyword", scenarioDefinition.getKeyword()); - testCaseMap.put("description", - scenarioDefinition.getDescription() != null ? scenarioDefinition.getDescription() : ""); - } - testCaseMap.put("steps", new ArrayList<Map<String, Object>>()); - if (!testCase.getTags().isEmpty()) { - List<Map<String, Object>> tagList = new ArrayList<>(); - for (String tag : testCase.getTags()) { - Map<String, Object> tagMap = new HashMap<>(); - tagMap.put("name", tag); - tagList.add(tagMap); - } - testCaseMap.put("tags", tagList); - } - return testCaseMap; - } - - private Map<String, Object> createBackground(TestCase testCase) { - TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLocation().getLine()); - if (astNode != null) { - Background background = getBackgroundForTestCase(astNode).get(); - Map<String, Object> testCaseMap = new HashMap<>(); - testCaseMap.put("name", background.getName()); - testCaseMap.put("line", background.getLocation().getLine()); - testCaseMap.put("type", "background"); - testCaseMap.put("keyword", background.getKeyword()); - testCaseMap.put("description", background.getDescription() != null ? background.getDescription() : ""); - testCaseMap.put("steps", new ArrayList<Map<String, Object>>()); - return testCaseMap; - } - return null; - } - - private boolean isFirstStepAfterBackground(PickleStepTestStep testStep) { - TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testStep.getStepLine()); - if (astNode == null) { - return false; - } - return currentElementMap != currentTestCaseMap && !TestSourcesModel.isBackgroundStep(astNode); - } - - private Map<String, Object> createTestStep(PickleStepTestStep testStep) { - Map<String, Object> stepMap = new HashMap<>(); - stepMap.put("name", testStep.getStepText()); - stepMap.put("line", testStep.getStepLine()); - TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testStep.getStepLine()); - StepArgument argument = testStep.getStepArgument(); - if (argument != null) { - if (argument instanceof DocStringArgument) { - DocStringArgument docStringArgument = (DocStringArgument) argument; - stepMap.put("doc_string", createDocStringMap(docStringArgument)); - } else if (argument instanceof DataTableArgument) { - DataTableArgument dataTableArgument = (DataTableArgument) argument; - stepMap.put("rows", createDataTableList(dataTableArgument)); - } - } - if (astNode != null) { - Step step = (Step) astNode.node; - stepMap.put("keyword", step.getKeyword()); + throw new IllegalStateException(e); } - return stepMap; - } - - private Map<String, Object> createHookStep(HookTestStep hookTestStep) { - return new HashMap<>(); - } - - private void addHookStepToTestCaseMap(Map<String, Object> currentStepOrHookMap, HookType hookType) { - String hookName; - if (hookType == HookType.AFTER || hookType == HookType.AFTER_STEP) - hookName = after; - else - hookName = before; - - Map<String, Object> mapToAddTo; - switch (hookType) { - case BEFORE: - mapToAddTo = currentTestCaseMap; - break; - case AFTER: - mapToAddTo = currentTestCaseMap; - break; - case BEFORE_STEP: - mapToAddTo = currentBeforeStepHookList; - break; - case AFTER_STEP: - mapToAddTo = currentStepsList.get(currentStepsList.size() - 1); - break; - default: - mapToAddTo = currentTestCaseMap; - } - - if (!mapToAddTo.containsKey(hookName)) { - mapToAddTo.put(hookName, new ArrayList<Map<String, Object>>()); - } - ((List<Map<String, Object>>) mapToAddTo.get(hookName)).add(currentStepOrHookMap); - } - - private Map<String, Object> createMatchMap(TestStep step, Result result) { - Map<String, Object> matchMap = new HashMap<>(); - if (step instanceof PickleStepTestStep) { - PickleStepTestStep testStep = (PickleStepTestStep) step; - if (!testStep.getDefinitionArgument().isEmpty()) { - List<Map<String, Object>> argumentList = new ArrayList<>(); - for (Argument argument : testStep.getDefinitionArgument()) { - Map<String, Object> argumentMap = new HashMap<>(); - if (argument.getValue() != null) { - argumentMap.put("val", argument.getValue()); - argumentMap.put("offset", argument.getStart()); - } - argumentList.add(argumentMap); - } - matchMap.put("arguments", argumentList); + // TODO: Plugins should implement the closable interface + // and be closed by Cucumber + if (event.getTestRunFinished().isPresent()) { + try { + writer.close(); + } catch (IOException e) { + throw new IllegalStateException(e); } } - if (!result.getStatus().is(Status.UNDEFINED)) { - matchMap.put("location", step.getCodeLocation()); - } - return matchMap; } - - private Map<String, Object> createResultMap(Result result) { - Map<String, Object> resultMap = new HashMap<>(); - resultMap.put("status", result.getStatus().name().toLowerCase(ROOT)); - if (result.getError() != null) { - resultMap.put("error_message", printStackTrace(result.getError())); - } - if (!result.getDuration().isZero()) { - resultMap.put("duration", result.getDuration().toNanos()); - } - return resultMap; - } - - private void addOutputToHookMap(String text) { - if (!currentStepOrHookMap.containsKey("output")) { - currentStepOrHookMap.put("output", new ArrayList<String>()); - } - ((List<String>) currentStepOrHookMap.get("output")).add(text); - } - - private void addEmbeddingToHookMap(byte[] data, String mediaType, String name) { - if (!currentStepOrHookMap.containsKey("embeddings")) { - currentStepOrHookMap.put("embeddings", new ArrayList<Map<String, Object>>()); - } - Map<String, Object> embedMap = createEmbeddingMap(data, mediaType, name); - ((List<Map<String, Object>>) currentStepOrHookMap.get("embeddings")).add(embedMap); - } - - private Map<String, Object> createDummyFeatureForFailure(TestRunFinished event) { - Throwable exception = event.getResult().getError(); - - Map<String, Object> feature = new LinkedHashMap<>(); - feature.put("line", 1); - { - Map<String, Object> scenario = new LinkedHashMap<>(); - feature.put("elements", singletonList(scenario)); - - scenario.put("start_timestamp", getDateTimeFromTimeStamp(event.getInstant())); - scenario.put("line", 2); - scenario.put("name", "Failure while executing Cucumber"); - scenario.put("description", ""); - scenario.put("id", "failure;failure-while-executing-cucumber"); - scenario.put("type", "scenario"); - scenario.put("keyword", "Scenario"); - - Map<String, Object> when = new LinkedHashMap<>(); - Map<String, Object> then = new LinkedHashMap<>(); - scenario.put("steps", Arrays.asList(when, then)); - { - - { - Map<String, Object> whenResult = new LinkedHashMap<>(); - when.put("result", whenResult); - whenResult.put("duration", 0); - whenResult.put("status", "passed"); - } - when.put("line", 3); - when.put("name", "Cucumber failed while executing"); - Map<String, Object> whenMatch = new LinkedHashMap<>(); - when.put("match", whenMatch); - whenMatch.put("arguments", new ArrayList<>()); - whenMatch.put("location", "io.cucumber.core.Failure.failure_while_executing_cucumber()"); - when.put("keyword", "When "); - - { - Map<String, Object> thenResult = new LinkedHashMap<>(); - then.put("result", thenResult); - thenResult.put("duration", 0); - thenResult.put("error_message", printStackTrace(exception)); - thenResult.put("status", "failed"); - } - then.put("line", 4); - then.put("name", "Cucumber will report this error:"); - Map<String, Object> thenMatch = new LinkedHashMap<>(); - then.put("match", thenMatch); - thenMatch.put("arguments", new ArrayList<>()); - thenMatch.put("location", "io.cucumber.core.Failure.cucumber_reports_this_error()"); - then.put("keyword", "Then "); - } - - feature.put("name", "Test run failed"); - feature.put("description", "There were errors during the execution"); - feature.put("id", "failure"); - feature.put("keyword", "Feature"); - feature.put("uri", "classpath:io/cucumber/core/failure.feature"); - feature.put("tags", new ArrayList<>()); - } - - return feature; - } - - private String getDateTimeFromTimeStamp(Instant instant) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - .withZone(ZoneOffset.UTC); - return formatter.format(instant); - } - - private Map<String, Object> createDocStringMap(DocStringArgument docString) { - Map<String, Object> docStringMap = new HashMap<>(); - docStringMap.put("value", docString.getContent()); - docStringMap.put("line", docString.getLine()); - docStringMap.put("content_type", docString.getMediaType()); - return docStringMap; - } - - private List<Map<String, List<String>>> createDataTableList(DataTableArgument argument) { - List<Map<String, List<String>>> rowList = new ArrayList<>(); - for (List<String> row : argument.cells()) { - Map<String, List<String>> rowMap = new HashMap<>(); - rowMap.put("cells", new ArrayList<>(row)); - rowList.add(rowMap); - } - return rowList; - } - - private Map<String, Object> createEmbeddingMap(byte[] data, String mediaType, String name) { - Map<String, Object> embedMap = new HashMap<>(); - embedMap.put("mime_type", mediaType); // Should be media-type but not - // worth migrating for - embedMap.put("data", Base64.getEncoder().encodeToString(data)); - if (name != null) { - embedMap.put("name", name); - } - return embedMap; - } - } diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatterOld.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatterOld.java new file mode 100644 index 0000000000..8b6d943480 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonFormatterOld.java @@ -0,0 +1,442 @@ +package io.cucumber.core.plugin; + +import io.cucumber.messages.types.Background; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.Scenario; +import io.cucumber.messages.types.Step; +import io.cucumber.plugin.EventListener; +import io.cucumber.plugin.event.Argument; +import io.cucumber.plugin.event.DataTableArgument; +import io.cucumber.plugin.event.DocStringArgument; +import io.cucumber.plugin.event.EmbedEvent; +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.Status; +import io.cucumber.plugin.event.StepArgument; +import io.cucumber.plugin.event.TestCase; +import io.cucumber.plugin.event.TestCaseStarted; +import io.cucumber.plugin.event.TestRunFinished; +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.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.net.URI; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static io.cucumber.core.exception.ExceptionUtils.printStackTrace; +import static io.cucumber.core.plugin.TestSourcesModel.getBackgroundForTestCase; +import static java.util.Collections.singletonList; +import static java.util.Locale.ROOT; +import static java.util.stream.Collectors.toList; + +public final class JsonFormatterOld implements EventListener { + + private static final String before = "before"; + private static final String after = "after"; + private final List<Map<String, Object>> featureMaps = new ArrayList<>(); + private final Map<String, Object> currentBeforeStepHookList = new HashMap<>(); + private final Writer writer; + private final TestSourcesModel testSources = new TestSourcesModel(); + private URI currentFeatureFile; + private List<Map<String, Object>> currentElementsList; + private Map<String, Object> currentElementMap; + private Map<String, Object> currentTestCaseMap; + private List<Map<String, Object>> currentStepsList; + private Map<String, Object> currentStepOrHookMap; + + @SuppressWarnings("WeakerAccess") // Used by PluginFactory + public JsonFormatterOld(OutputStream out) { + this.writer = new UTF8OutputStreamWriter(out); + } + + @Override + public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(TestSourceRead.class, this::handleTestSourceRead); + publisher.registerHandlerFor(TestCaseStarted.class, this::handleTestCaseStarted); + publisher.registerHandlerFor(TestStepStarted.class, this::handleTestStepStarted); + publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished); + publisher.registerHandlerFor(WriteEvent.class, this::handleWrite); + publisher.registerHandlerFor(EmbedEvent.class, this::handleEmbed); + publisher.registerHandlerFor(TestRunFinished.class, this::finishReport); + } + + private void handleTestSourceRead(TestSourceRead event) { + testSources.addTestSourceReadEvent(event.getUri(), event); + } + + @SuppressWarnings("unchecked") + private void handleTestCaseStarted(TestCaseStarted event) { + if (currentFeatureFile == null || !currentFeatureFile.equals(event.getTestCase().getUri())) { + currentFeatureFile = event.getTestCase().getUri(); + Map<String, Object> currentFeatureMap = createFeatureMap(event.getTestCase()); + featureMaps.add(currentFeatureMap); + currentElementsList = (List<Map<String, Object>>) currentFeatureMap.get("elements"); + } + currentTestCaseMap = createTestCase(event); + if (testSources.hasBackground(currentFeatureFile, event.getTestCase().getLocation().getLine())) { + currentElementMap = createBackground(event.getTestCase()); + currentElementsList.add(currentElementMap); + } else { + currentElementMap = currentTestCaseMap; + } + currentElementsList.add(currentTestCaseMap); + currentStepsList = (List<Map<String, Object>>) currentElementMap.get("steps"); + } + + @SuppressWarnings("unchecked") + private void handleTestStepStarted(TestStepStarted event) { + if (event.getTestStep() instanceof PickleStepTestStep) { + PickleStepTestStep testStep = (PickleStepTestStep) event.getTestStep(); + if (isFirstStepAfterBackground(testStep)) { + currentElementMap = currentTestCaseMap; + currentStepsList = (List<Map<String, Object>>) currentElementMap.get("steps"); + } + currentStepOrHookMap = createTestStep(testStep); + // add beforeSteps list to current step + if (currentBeforeStepHookList.containsKey(before)) { + currentStepOrHookMap.put(before, currentBeforeStepHookList.get(before)); + currentBeforeStepHookList.clear(); + } + currentStepsList.add(currentStepOrHookMap); + } else if (event.getTestStep() instanceof HookTestStep) { + HookTestStep hookTestStep = (HookTestStep) event.getTestStep(); + currentStepOrHookMap = createHookStep(hookTestStep); + addHookStepToTestCaseMap(currentStepOrHookMap, hookTestStep.getHookType()); + } else { + throw new IllegalStateException(); + } + } + + private void handleTestStepFinished(TestStepFinished event) { + currentStepOrHookMap.put("match", createMatchMap(event.getTestStep(), event.getResult())); + currentStepOrHookMap.put("result", createResultMap(event.getResult())); + } + + private void handleWrite(WriteEvent event) { + addOutputToHookMap(event.getText()); + } + + private void handleEmbed(EmbedEvent event) { + addEmbeddingToHookMap(event.getData(), event.getMediaType(), event.getName()); + } + + private void finishReport(TestRunFinished event) { + Throwable exception = event.getResult().getError(); + if (exception != null) { + featureMaps.add(createDummyFeatureForFailure(event)); + } + + try { + Jackson.OBJECT_MAPPER.writeValue(writer, featureMaps); + writer.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Map<String, Object> createFeatureMap(TestCase testCase) { + Map<String, Object> featureMap = new HashMap<>(); + featureMap.put("uri", TestSourcesModel.relativize(testCase.getUri())); + featureMap.put("elements", new ArrayList<Map<String, Object>>()); + Feature feature = testSources.getFeature(testCase.getUri()); + if (feature != null) { + featureMap.put("keyword", feature.getKeyword()); + featureMap.put("name", feature.getName()); + featureMap.put("description", feature.getDescription() != null ? feature.getDescription() : ""); + featureMap.put("line", feature.getLocation().getLine()); + featureMap.put("id", TestSourcesModel.convertToId(feature.getName())); + featureMap.put("tags", feature.getTags().stream().map( + tag -> { + Map<String, Object> json = new LinkedHashMap<>(); + json.put("name", tag.getName()); + json.put("type", "Tag"); + Map<String, Object> location = new LinkedHashMap<>(); + location.put("line", tag.getLocation().getLine()); + location.put("column", tag.getLocation().getColumn()); + json.put("location", location); + return json; + }).collect(toList())); + + } + return featureMap; + } + + private Map<String, Object> createTestCase(TestCaseStarted event) { + Map<String, Object> testCaseMap = new HashMap<>(); + + testCaseMap.put("start_timestamp", getDateTimeFromTimeStamp(event.getInstant())); + + TestCase testCase = event.getTestCase(); + + testCaseMap.put("name", testCase.getName()); + testCaseMap.put("line", testCase.getLine()); + testCaseMap.put("type", "scenario"); + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLine()); + if (astNode != null) { + testCaseMap.put("id", TestSourcesModel.calculateId(astNode)); + Scenario scenarioDefinition = TestSourcesModel.getScenarioDefinition(astNode); + testCaseMap.put("keyword", scenarioDefinition.getKeyword()); + testCaseMap.put("description", + scenarioDefinition.getDescription() != null ? scenarioDefinition.getDescription() : ""); + } + testCaseMap.put("steps", new ArrayList<Map<String, Object>>()); + if (!testCase.getTags().isEmpty()) { + List<Map<String, Object>> tagList = new ArrayList<>(); + for (String tag : testCase.getTags()) { + Map<String, Object> tagMap = new HashMap<>(); + tagMap.put("name", tag); + tagList.add(tagMap); + } + testCaseMap.put("tags", tagList); + } + return testCaseMap; + } + + private Map<String, Object> createBackground(TestCase testCase) { + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLocation().getLine()); + if (astNode != null) { + Background background = getBackgroundForTestCase(astNode).get(); + Map<String, Object> testCaseMap = new HashMap<>(); + testCaseMap.put("name", background.getName()); + testCaseMap.put("line", background.getLocation().getLine()); + testCaseMap.put("type", "background"); + testCaseMap.put("keyword", background.getKeyword()); + testCaseMap.put("description", background.getDescription() != null ? background.getDescription() : ""); + testCaseMap.put("steps", new ArrayList<Map<String, Object>>()); + return testCaseMap; + } + return null; + } + + private boolean isFirstStepAfterBackground(PickleStepTestStep testStep) { + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testStep.getStepLine()); + if (astNode == null) { + return false; + } + return currentElementMap != currentTestCaseMap && !TestSourcesModel.isBackgroundStep(astNode); + } + + private Map<String, Object> createTestStep(PickleStepTestStep testStep) { + Map<String, Object> stepMap = new HashMap<>(); + stepMap.put("name", testStep.getStepText()); + stepMap.put("line", testStep.getStepLine()); + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testStep.getStepLine()); + StepArgument argument = testStep.getStepArgument(); + if (argument != null) { + if (argument instanceof DocStringArgument) { + DocStringArgument docStringArgument = (DocStringArgument) argument; + stepMap.put("doc_string", createDocStringMap(docStringArgument)); + } else if (argument instanceof DataTableArgument) { + DataTableArgument dataTableArgument = (DataTableArgument) argument; + stepMap.put("rows", createDataTableList(dataTableArgument)); + } + } + if (astNode != null) { + Step step = (Step) astNode.node; + stepMap.put("keyword", step.getKeyword()); + } + + return stepMap; + } + + private Map<String, Object> createHookStep(HookTestStep hookTestStep) { + return new HashMap<>(); + } + + private void addHookStepToTestCaseMap(Map<String, Object> currentStepOrHookMap, HookType hookType) { + String hookName; + if (hookType == HookType.AFTER || hookType == HookType.AFTER_STEP) + hookName = after; + else + hookName = before; + + Map<String, Object> mapToAddTo; + switch (hookType) { + case BEFORE: + mapToAddTo = currentTestCaseMap; + break; + case AFTER: + mapToAddTo = currentTestCaseMap; + break; + case BEFORE_STEP: + mapToAddTo = currentBeforeStepHookList; + break; + case AFTER_STEP: + mapToAddTo = currentStepsList.get(currentStepsList.size() - 1); + break; + default: + mapToAddTo = currentTestCaseMap; + } + + if (!mapToAddTo.containsKey(hookName)) { + mapToAddTo.put(hookName, new ArrayList<Map<String, Object>>()); + } + ((List<Map<String, Object>>) mapToAddTo.get(hookName)).add(currentStepOrHookMap); + } + + private Map<String, Object> createMatchMap(TestStep step, Result result) { + Map<String, Object> matchMap = new HashMap<>(); + if (step instanceof PickleStepTestStep) { + PickleStepTestStep testStep = (PickleStepTestStep) step; + if (!testStep.getDefinitionArgument().isEmpty()) { + List<Map<String, Object>> argumentList = new ArrayList<>(); + for (Argument argument : testStep.getDefinitionArgument()) { + Map<String, Object> argumentMap = new HashMap<>(); + if (argument.getValue() != null) { + argumentMap.put("val", argument.getValue()); + argumentMap.put("offset", argument.getStart()); + } + argumentList.add(argumentMap); + } + matchMap.put("arguments", argumentList); + } + } + if (!result.getStatus().is(Status.UNDEFINED)) { + matchMap.put("location", step.getCodeLocation()); + } + return matchMap; + } + + private Map<String, Object> createResultMap(Result result) { + Map<String, Object> resultMap = new HashMap<>(); + resultMap.put("status", result.getStatus().name().toLowerCase(ROOT)); + if (result.getError() != null) { + resultMap.put("error_message", printStackTrace(result.getError())); + } + if (!result.getDuration().isZero()) { + resultMap.put("duration", result.getDuration().toNanos()); + } + return resultMap; + } + + private void addOutputToHookMap(String text) { + if (!currentStepOrHookMap.containsKey("output")) { + currentStepOrHookMap.put("output", new ArrayList<String>()); + } + ((List<String>) currentStepOrHookMap.get("output")).add(text); + } + + private void addEmbeddingToHookMap(byte[] data, String mediaType, String name) { + if (!currentStepOrHookMap.containsKey("embeddings")) { + currentStepOrHookMap.put("embeddings", new ArrayList<Map<String, Object>>()); + } + Map<String, Object> embedMap = createEmbeddingMap(data, mediaType, name); + ((List<Map<String, Object>>) currentStepOrHookMap.get("embeddings")).add(embedMap); + } + + private Map<String, Object> createDummyFeatureForFailure(TestRunFinished event) { + Throwable exception = event.getResult().getError(); + + Map<String, Object> feature = new LinkedHashMap<>(); + feature.put("line", 1); + { + Map<String, Object> scenario = new LinkedHashMap<>(); + feature.put("elements", singletonList(scenario)); + + scenario.put("start_timestamp", getDateTimeFromTimeStamp(event.getInstant())); + scenario.put("line", 2); + scenario.put("name", "Failure while executing Cucumber"); + scenario.put("description", ""); + scenario.put("id", "failure;failure-while-executing-cucumber"); + scenario.put("type", "scenario"); + scenario.put("keyword", "Scenario"); + + Map<String, Object> when = new LinkedHashMap<>(); + Map<String, Object> then = new LinkedHashMap<>(); + scenario.put("steps", Arrays.asList(when, then)); + { + + { + Map<String, Object> whenResult = new LinkedHashMap<>(); + when.put("result", whenResult); + whenResult.put("duration", 0); + whenResult.put("status", "passed"); + } + when.put("line", 3); + when.put("name", "Cucumber failed while executing"); + Map<String, Object> whenMatch = new LinkedHashMap<>(); + when.put("match", whenMatch); + whenMatch.put("arguments", new ArrayList<>()); + whenMatch.put("location", "io.cucumber.core.Failure.failure_while_executing_cucumber()"); + when.put("keyword", "When "); + + { + Map<String, Object> thenResult = new LinkedHashMap<>(); + then.put("result", thenResult); + thenResult.put("duration", 0); + thenResult.put("error_message", printStackTrace(exception)); + thenResult.put("status", "failed"); + } + then.put("line", 4); + then.put("name", "Cucumber will report this error:"); + Map<String, Object> thenMatch = new LinkedHashMap<>(); + then.put("match", thenMatch); + thenMatch.put("arguments", new ArrayList<>()); + thenMatch.put("location", "io.cucumber.core.Failure.cucumber_reports_this_error()"); + then.put("keyword", "Then "); + } + + feature.put("name", "Test run failed"); + feature.put("description", "There were errors during the execution"); + feature.put("id", "failure"); + feature.put("keyword", "Feature"); + feature.put("uri", "classpath:io/cucumber/core/failure.feature"); + feature.put("tags", new ArrayList<>()); + } + + return feature; + } + + private String getDateTimeFromTimeStamp(Instant instant) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + .withZone(ZoneOffset.UTC); + return formatter.format(instant); + } + + private Map<String, Object> createDocStringMap(DocStringArgument docString) { + Map<String, Object> docStringMap = new HashMap<>(); + docStringMap.put("value", docString.getContent()); + docStringMap.put("line", docString.getLine()); + docStringMap.put("content_type", docString.getMediaType()); + return docStringMap; + } + + private List<Map<String, List<String>>> createDataTableList(DataTableArgument argument) { + List<Map<String, List<String>>> rowList = new ArrayList<>(); + for (List<String> row : argument.cells()) { + Map<String, List<String>> rowMap = new HashMap<>(); + rowMap.put("cells", new ArrayList<>(row)); + rowList.add(rowMap); + } + return rowList; + } + + private Map<String, Object> createEmbeddingMap(byte[] data, String mediaType, String name) { + Map<String, Object> embedMap = new HashMap<>(); + embedMap.put("mime_type", mediaType); // Should be media-type but not + // worth migrating for + embedMap.put("data", Base64.getEncoder().encodeToString(data)); + if (name != null) { + embedMap.put("name", name); + } + return embedMap; + } + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportData.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportData.java new file mode 100644 index 0000000000..de71b21cae --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportData.java @@ -0,0 +1,13 @@ +package io.cucumber.core.plugin; + +import io.cucumber.messages.types.Envelope; +import io.cucumber.query.Query; + +class JsonReportData { + + private final Query query = new Query(); + + void collect(Envelope envelope) { + query.update(envelope); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportWriter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportWriter.java new file mode 100644 index 0000000000..290c9b4cb0 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportWriter.java @@ -0,0 +1,409 @@ +package io.cucumber.core.plugin; + +import io.cucumber.core.plugin.CucumberJvmJson.JvmArgument; +import io.cucumber.core.plugin.CucumberJvmJson.JvmDataTableRow; +import io.cucumber.core.plugin.CucumberJvmJson.JvmDocString; +import io.cucumber.core.plugin.CucumberJvmJson.JvmElement; +import io.cucumber.core.plugin.CucumberJvmJson.JvmElementType; +import io.cucumber.core.plugin.CucumberJvmJson.JvmFeature; +import io.cucumber.core.plugin.CucumberJvmJson.JvmLocation; +import io.cucumber.core.plugin.CucumberJvmJson.JvmLocationTag; +import io.cucumber.core.plugin.CucumberJvmJson.JvmMatch; +import io.cucumber.core.plugin.CucumberJvmJson.JvmResult; +import io.cucumber.core.plugin.CucumberJvmJson.JvmStatus; +import io.cucumber.core.plugin.CucumberJvmJson.JvmStep; +import io.cucumber.core.plugin.CucumberJvmJson.JvmTag; +import io.cucumber.messages.Convertor; +import io.cucumber.messages.types.Attachment; +import io.cucumber.messages.types.Background; +import io.cucumber.messages.types.DataTable; +import io.cucumber.messages.types.DocString; +import io.cucumber.messages.types.Exception; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.GherkinDocument; +import io.cucumber.messages.types.Group; +import io.cucumber.messages.types.Hook; +import io.cucumber.messages.types.HookType; +import io.cucumber.messages.types.JavaMethod; +import io.cucumber.messages.types.JavaStackTraceElement; +import io.cucumber.messages.types.Pickle; +import io.cucumber.messages.types.PickleStep; +import io.cucumber.messages.types.Rule; +import io.cucumber.messages.types.RuleChild; +import io.cucumber.messages.types.Scenario; +import io.cucumber.messages.types.SourceReference; +import io.cucumber.messages.types.Step; +import io.cucumber.messages.types.StepDefinition; +import io.cucumber.messages.types.StepMatchArgumentsList; +import io.cucumber.messages.types.TableCell; +import io.cucumber.messages.types.Tag; +import io.cucumber.messages.types.TestCaseStarted; +import io.cucumber.messages.types.TestStep; +import io.cucumber.messages.types.TestStepFinished; +import io.cucumber.messages.types.TestStepResult; +import io.cucumber.messages.types.TestStepResultStatus; +import io.cucumber.messages.types.Timestamp; +import io.cucumber.query.NamingStrategy; +import io.cucumber.query.Query; + +import java.net.URI; +import java.time.Duration; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import static io.cucumber.core.plugin.TestSourcesModel.convertToId; +import static io.cucumber.messages.types.HookType.AFTER; +import static io.cucumber.messages.types.HookType.BEFORE; +import static java.util.Locale.ROOT; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; + +class JsonReportWriter { + private final Query query; + + JsonReportWriter(Query query) { + this.query = query; + } + + List<Object> writeJsonReport() { + return query.findAllTestCaseStartedGroupedByFeature() + .entrySet() + .stream() + .map(this::createFeatureMap) + .collect(toList()); + } + + private JvmFeature createFeatureMap(Entry<Optional<Feature>, List<TestCaseStarted>> entry) { + GherkinDocument document = getGherkinDocument(entry); + Feature feature = entry.getKey().get(); + return new JvmFeature( + TestSourcesModel.relativize(URI.create(document.getUri().get())).toString(), // TODO: Relativize, optional?, null? + convertToId(feature.getName()), + feature.getLocation().getLine(), + feature.getKeyword(), + feature.getName(), + feature.getDescription() != null ? feature.getDescription() : "", // TODO: Can this be null? + writeElementsReport(entry), + feature.getTags().stream() + .map(JsonReportWriter::createLocationTag) + .collect(toList()) + ); + } + + private static JvmLocationTag createLocationTag(Tag tag) { + return new JvmLocationTag( + tag.getName(), + "Tag", + new JvmLocation( + tag.getLocation().getLine(), + tag.getLocation().getColumn().orElse(0L) + ) + ); + } + + private GherkinDocument getGherkinDocument(Entry<Optional<Feature>, List<TestCaseStarted>> entry) { + return entry.getValue().stream() + .findFirst() + .flatMap(query::findGherkinDocumentBy) + .orElseThrow(() -> new IllegalArgumentException("No Gherkin document")); + } + + private List<JvmElement> writeElementsReport(Entry<Optional<Feature>, List<TestCaseStarted>> entry) { + return entry.getValue().stream() + .map(this::createTestCaseAndBackGround) + .flatMap(Collection::stream) + .collect(toList()); + } + + private List<JvmElement> createTestCaseAndBackGround(TestCaseStarted testCaseStarted) { + // TODO: Clean up + Predicate<Entry<Optional<Background>, List<TestStepFinished>>> isBackGround = entry -> entry.getKey().isPresent(); + Predicate<Entry<Optional<Background>, List<TestStepFinished>>> isTestCase = isBackGround.negate(); + BinaryOperator<Entry<Optional<Background>, List<TestStepFinished>>> mergeSteps = (a, b) -> { + a.getValue().addAll(b.getValue()); + return a; + }; + Map<Optional<Background>, List<TestStepFinished>> stepsByBackground = query.findTestStepFinishedAndTestStepBy(testCaseStarted) + .stream() + .collect(groupByBackground(testCaseStarted)); + + // There can be multiple backgrounds, but historically the json format + // only ever had one. So we group all other backgrounds steps with the + // first. + Optional<JvmElement> background = stepsByBackground.entrySet().stream() + .filter(isBackGround) + .reduce(mergeSteps) + .flatMap(entry -> entry.getKey().map(bg -> createBackground(bg, entry.getValue()))); + + Optional<JvmElement> testCase = stepsByBackground.entrySet().stream() + .filter(isTestCase) + .reduce(mergeSteps) + .map(Entry::getValue) + .map(testStepFinished -> createTestCase(testCaseStarted, testStepFinished)); + + return Stream.of(background, testCase) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + + private Collector<Entry<TestStepFinished, TestStep>, ?, Map<Optional<Background>, List<TestStepFinished>>> groupByBackground(TestCaseStarted testCaseStarted) { + List<Background> backgrounds = query.findFeatureBy(testCaseStarted) + .map(this::getBackgroundsBy) + .orElseGet(Collections::emptyList); + + Function<Entry<TestStepFinished, TestStep>, Optional<Background>> grouping = + entry -> query.findPickleStepBy(entry.getValue()) + .flatMap(pickleStep -> findBackgroundBy(backgrounds, pickleStep)); + + Function<List<Entry<TestStepFinished, TestStep>>, List<TestStepFinished>> extractKey = entries -> entries.stream() + .map(Entry::getKey) + .collect(toList()); + + return groupingBy(grouping, LinkedHashMap::new, collectingAndThen(toList(), extractKey)); + } + + private static Optional<Background> findBackgroundBy(List<Background> backgrounds, PickleStep pickleStep) { + return backgrounds.stream() + .filter(background -> background.getSteps().stream() + .map(Step::getId) + .anyMatch(step -> pickleStep.getAstNodeIds().contains(step))) + .findFirst(); + } + + private List<Background> getBackgroundsBy(Feature feature) { + return feature.getChildren() + .stream() + .map(featureChild -> { + List<Background> backgrounds = new ArrayList<>(); + featureChild.getBackground().ifPresent(backgrounds::add); + featureChild.getRule() + .map(Rule::getChildren) + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(RuleChild::getBackground) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(backgrounds::add); + return backgrounds; + }) + .flatMap(Collection::stream) + .collect(toList()); + } + + private JvmElement createBackground(Background background, List<TestStepFinished> testStepsFinished) { + return new JvmElement( + null, + background.getLocation().getLine(), + null, + JvmElementType.background, + background.getKeyword(), + background.getName(), + background.getDescription() != null ? background.getDescription() : "", + createTestSteps(testStepsFinished), + null, + null, + null + ); + } + + + private JvmElement createTestCase(TestCaseStarted event, List<TestStepFinished> testStepsFinished) { + Pickle pickle = query.findPickleBy(event).orElseThrow(); + Scenario scenario = query.findScenarioBy(event).orElseThrow(); + NamingStrategy idStrategy = NamingStrategy.strategy(NamingStrategy.Strategy.LONG).delimiter(";").namingVisitor(new IdNamingVisitor()).build(); + List<CucumberJvmJson.JvmHook> beforeHooks = createHookSteps(testStepsFinished, include(BEFORE)); + List<CucumberJvmJson.JvmHook> afterHooks = createHookSteps(testStepsFinished, include(AFTER)); + return new JvmElement( + getDateTimeFromTimeStamp(event.getTimestamp()), + query.findLocationOf(pickle).orElseThrow().getLine(), + query.findNameOf(pickle, idStrategy), + JvmElementType.scenario, + scenario.getKeyword(), + pickle.getName(), + scenario.getDescription() != null ? scenario.getDescription() : "", + createTestSteps(testStepsFinished), + beforeHooks.isEmpty() ? null : beforeHooks, + afterHooks.isEmpty() ? null : afterHooks, + pickle.getTags().isEmpty() ? null : createTags(pickle) + ); + } + + private List<CucumberJvmJson.JvmHook> createHookSteps(List<TestStepFinished> testStepsFinished, Predicate<Hook> predicate) { + return testStepsFinished.stream() + .map(testStepFinished -> query.findTestStepBy(testStepFinished) + .flatMap(testStep -> query.findHookBy(testStep) + .filter(predicate) + .map(hook -> new CucumberJvmJson.JvmHook( + createMatchMap(testStep, testStepFinished.getTestStepResult()), + createResultMap(testStepFinished.getTestStepResult()), + createEmbeddings(query.findAllAttachmentsBy(testStep)) + )))) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + + private List<CucumberJvmJson.JvmEmbedding> createEmbeddings(List<Attachment> attachments) { + if (attachments.isEmpty()) { + return null; + } + return attachments.stream() + .map(attachment -> new CucumberJvmJson.JvmEmbedding( + attachment.getMediaType(), + attachment.getBody(), + attachment.getFileName().orElse(null) + )).collect(toList()); + } + + private List<JvmStep> createTestSteps(List<TestStepFinished> testStepsFinished) { + return testStepsFinished.stream() + .map(this::createTestStep) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + + private static List<JvmTag> createTags(Pickle pickle) { + return pickle.getTags().stream().map(pickleTag -> new JvmTag(pickleTag.getName())).collect(toList()); + } + + private Optional<JvmStep> createTestStep(TestStepFinished testStepFinished) { + return query.findTestStepBy(testStepFinished) + .flatMap(testStep -> query.findPickleStepBy(testStep) + .flatMap(pickleStep -> query.findStepBy(pickleStep) + .map(step -> new JvmStep( + step.getKeyword(), + step.getLocation().getLine(), + createMatchMap(testStep, testStepFinished.getTestStepResult()), + pickleStep.getText(), + createResultMap(testStepFinished.getTestStepResult()), + step.getDocString().map(this::createDocStringMap).orElse(null), + step.getDataTable().map(this::createDataTableList).orElse(null) + )) + ) + ); + } + + private static Predicate<Hook> include(HookType... hookTypes) { + List<HookType> keep = Arrays.asList(hookTypes); + return hook -> hook.getType().map(keep::contains).orElse(false); + } + + private static Predicate<Hook> exclude(HookType... hookTypes) { + return include(hookTypes).negate(); + } + + private JvmMatch createMatchMap(TestStep step, TestStepResult result) { + Optional<SourceReference> source = query.findStepDefinitionBy(step) + .stream() + .findFirst() + .map(StepDefinition::getSourceReference); + + Optional<String> location = source.flatMap(sourceReference -> { + Optional<String> fromUri = sourceReference.getUri() + .map(uri -> renderLocationString(sourceReference, uri)); + + Optional<String> fromJavaMethod = sourceReference.getJavaMethod() + .map(JsonReportWriter::renderLocationString); + + Optional<String> fromStackTrace = sourceReference.getJavaStackTraceElement() + .map(javaStackTraceElement -> renderLocationString(sourceReference, javaStackTraceElement)); + + return Stream.of(fromStackTrace, fromJavaMethod, fromUri).filter(Optional::isPresent).map(Optional::get).findFirst(); + }); + + Optional<List<JvmArgument>> argumentList = step.getStepMatchArgumentsLists() + .map(argumentsLists -> argumentsLists.stream() + .map(StepMatchArgumentsList::getStepMatchArguments) + .flatMap(Collection::stream) + .map(argument -> { + Group group = argument.getGroup(); + return new JvmArgument( + // TODO: Nullable + group.getValue().get(), + group.getStart().get() + ); + }).collect(toList())) + .filter(maps -> !maps.isEmpty()); + + return new JvmMatch( + result.getStatus() != TestStepResultStatus.UNDEFINED ? location.orElse(null) : null, + argumentList.orElse(null) + ); + } + + private static String renderLocationString(SourceReference sourceReference, String uri) { + String locationLine = sourceReference.getLocation().map(location -> ":" + location.getLine()).orElse(""); + return uri + locationLine; + } + + private static String renderLocationString(SourceReference sourceReference, JavaStackTraceElement javaStackTraceElement) { + String locationLine = sourceReference.getLocation().map(location -> ":" + location.getLine()).orElse(""); + String argumentList = String.join(",", javaStackTraceElement.getFileName()); + return String.format( + "%s#%s(%s%s)", + javaStackTraceElement.getClassName(), + javaStackTraceElement.getMethodName(), + argumentList, + locationLine + ); + } + + private static String renderLocationString(JavaMethod javaMethod) { + return String.format( + "%s#%s(%s)", + javaMethod.getClassName(), + javaMethod.getMethodName(), + String.join(",", javaMethod.getMethodParameterTypes()) + ); + } + + private JvmResult createResultMap(TestStepResult result) { + Duration duration = Convertor.toDuration(result.getDuration()); + return new JvmResult( + duration.isZero() ? null : duration.toNanos(), + JvmStatus.valueOf(result.getStatus().name().toLowerCase(ROOT)), + result.getException().flatMap(Exception::getStackTrace).orElse(null) + ); + } + + private JvmDocString createDocStringMap(DocString docString) { + return new JvmDocString( + docString.getLocation().getLine(), + docString.getContent(), + docString.getMediaType().orElse(null) + ); + } + + private List<JvmDataTableRow> createDataTableList(DataTable argument) { + return argument.getRows().stream() + .map(row -> new JvmDataTableRow(row.getCells().stream() + .map(TableCell::getValue) + .collect(toList()))) + .collect(toList()); + } + + private String getDateTimeFromTimeStamp(Timestamp instant) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + .withZone(ZoneOffset.UTC); + return formatter.format(Convertor.toInstant(instant)); + } + + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/MessagesToJsonWriter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/MessagesToJsonWriter.java new file mode 100644 index 0000000000..3205a1700f --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/MessagesToJsonWriter.java @@ -0,0 +1,81 @@ +package io.cucumber.core.plugin; + + +import io.cucumber.messages.types.Envelope; +import io.cucumber.query.Query; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * Writes the message output of a test run as single json report. + * <p> + * Note: Messages are first collected and only written once the stream is closed. + */ +public class MessagesToJsonWriter implements AutoCloseable { + + private final OutputStreamWriter out; + private final Query query = new Query(); + private final Serializer serializer; + private boolean streamClosed = false; + + public MessagesToJsonWriter(OutputStream out, Serializer serializer) { + this.out = new OutputStreamWriter( + requireNonNull(out), + StandardCharsets.UTF_8 + ); + this.serializer = serializer; + } + + + /** + * Writes a cucumber message to the xml output. + * + * @param envelope the message + * @throws IOException if an IO error occurs + */ + public void write(Envelope envelope) throws IOException { + if (streamClosed) { + throw new IOException("Stream closed"); + } + query.update(envelope); + } + + /** + * Closes the stream, flushing it first. Once closed further write() + * invocations will cause an IOException to be thrown. Closing a closed + * stream has no effect. + * + * @throws IOException if an IO error occurs + */ + @Override + public void close() throws IOException { + if (streamClosed) { + return; + } + try { + List<Object> report = new JsonReportWriter(query).writeJsonReport(); + serializer.writeValue(out, report); + } finally { + try { + out.close(); + } finally { + streamClosed = true; + } + } + } + + @FunctionalInterface + public interface Serializer { + + void writeValue(Writer writer, Object value) throws IOException; + + } +} + diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/CachingGlue.java b/cucumber-core/src/main/java/io/cucumber/core/runner/CachingGlue.java index 5962758d5b..e3a818003e 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/CachingGlue.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/CachingGlue.java @@ -28,6 +28,7 @@ import io.cucumber.datatable.TableEntryByTypeTransformer; import io.cucumber.messages.types.Envelope; import io.cucumber.messages.types.Hook; +import io.cucumber.messages.types.HookType; import io.cucumber.messages.types.JavaMethod; import io.cucumber.messages.types.JavaStackTraceElement; import io.cucumber.messages.types.Location; @@ -47,6 +48,11 @@ import java.util.Map; import java.util.TreeMap; +import static io.cucumber.messages.types.HookType.AFTER; +import static io.cucumber.messages.types.HookType.AFTER_STEP; +import static io.cucumber.messages.types.HookType.BEFORE; +import static io.cucumber.messages.types.HookType.BEFORE_STEP; + final class CachingGlue implements Glue { private static final Comparator<CoreHookDefinition> HOOK_ORDER_ASCENDING = Comparator @@ -266,8 +272,8 @@ void prepareGlue(StepTypeRegistry stepTypeRegistry) throws DuplicateStepDefiniti // TODO: Redefine hooks for each scenario, similar to how we're doing // for CoreStepDefinition - beforeHooks.forEach(this::emitHook); - beforeStepHooks.forEach(this::emitHook); + beforeHooks.forEach(hook -> emitHook(hook, BEFORE)); + beforeStepHooks.forEach(hook -> emitHook(hook, BEFORE_STEP)); stepDefinitions.forEach(stepDefinition -> { StepExpression expression = stepExpressionFactory.createExpression(stepDefinition); @@ -281,8 +287,8 @@ void prepareGlue(StepTypeRegistry stepTypeRegistry) throws DuplicateStepDefiniti emitStepDefined(coreStepDefinition); }); - afterStepHooks.forEach(this::emitHook); - afterHooks.forEach(this::emitHook); + afterStepHooks.forEach(hook -> emitHook(hook, AFTER_STEP)); + afterHooks.forEach(hook -> emitHook(hook, AFTER)); } private void emitParameterTypeDefined(ParameterTypeDefinition parameterTypeDefinition) { @@ -299,14 +305,14 @@ private void emitParameterTypeDefined(ParameterTypeDefinition parameterTypeDefin bus.send(Envelope.of(messagesParameterType)); } - private void emitHook(CoreHookDefinition coreHook) { + private void emitHook(CoreHookDefinition coreHook, HookType type) { Hook messagesHook = new Hook( coreHook.getId().toString(), null, coreHook.getDefinitionLocation() .map(this::createSourceReference) .orElseGet(this::emptySourceReference), - coreHook.getTagExpression()); + coreHook.getTagExpression(), type); bus.send(Envelope.of(messagesHook)); } diff --git a/cucumber-core/src/test/java/io/cucumber/core/backend/StubHookDefinition.java b/cucumber-core/src/test/java/io/cucumber/core/backend/StubHookDefinition.java index 2ae7a4a5ed..3431066e6a 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/backend/StubHookDefinition.java +++ b/cucumber-core/src/test/java/io/cucumber/core/backend/StubHookDefinition.java @@ -1,16 +1,23 @@ package io.cucumber.core.backend; +import java.lang.reflect.Method; +import java.util.Optional; import java.util.function.Consumer; public class StubHookDefinition implements HookDefinition { private static final String STUBBED_LOCATION_WITH_DETAILS = "{stubbed location with details}"; - private final String location; + private final StubLocation location; private final RuntimeException exception; private final Consumer<TestCaseState> action; + public StubHookDefinition(Method location, RuntimeException exception, Consumer<TestCaseState> action) { + this.location = new StubLocation(location); + this.exception = exception; + this.action = action; + } public StubHookDefinition(String location, RuntimeException exception, Consumer<TestCaseState> action) { - this.location = location; + this.location = new StubLocation(location); this.exception = exception; this.action = action; } @@ -19,6 +26,10 @@ public StubHookDefinition(String location, Consumer<TestCaseState> action) { this(location, null, action); } + public StubHookDefinition(Method location, Consumer<TestCaseState> action) { + this(location, null, action); + } + public StubHookDefinition() { this(STUBBED_LOCATION_WITH_DETAILS, null, null); } @@ -35,6 +46,10 @@ public StubHookDefinition(String location) { this(location, null, null); } + public StubHookDefinition(Method location) { + this(location, null, null); + } + @Override public void execute(TestCaseState state) { if (action != null) { @@ -62,7 +77,11 @@ public boolean isDefinedAt(StackTraceElement stackTraceElement) { @Override public String getLocation() { - return location; + return location.getLocation(); } + @Override + public Optional<SourceReference> getSourceReference() { + return location.getSourceReference(); + } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/backend/StubLocation.java b/cucumber-core/src/test/java/io/cucumber/core/backend/StubLocation.java index 63b03f418b..dc63ac69ee 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/backend/StubLocation.java +++ b/cucumber-core/src/test/java/io/cucumber/core/backend/StubLocation.java @@ -1,11 +1,21 @@ package io.cucumber.core.backend; +import java.lang.reflect.Method; +import java.util.Optional; + public class StubLocation implements Located { private final String location; + private final SourceReference sourceReference; public StubLocation(String location) { this.location = location; + this.sourceReference = null; + } + + public StubLocation(Method method) { + this.location = null; + this.sourceReference = SourceReference.fromMethod(method); } @Override @@ -13,6 +23,11 @@ public boolean isDefinedAt(StackTraceElement stackTraceElement) { return false; } + @Override + public Optional<SourceReference> getSourceReference() { + return Optional.ofNullable(sourceReference); + } + @Override public String getLocation() { return location; diff --git a/cucumber-core/src/test/java/io/cucumber/core/backend/StubStepDefinition.java b/cucumber-core/src/test/java/io/cucumber/core/backend/StubStepDefinition.java index c09c631296..48084e7a9c 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/backend/StubStepDefinition.java +++ b/cucumber-core/src/test/java/io/cucumber/core/backend/StubStepDefinition.java @@ -1,8 +1,10 @@ package io.cucumber.core.backend; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -20,6 +22,10 @@ public StubStepDefinition(String pattern, String location, Type... types) { this(pattern, location, null, types); } + public StubStepDefinition(String pattern, Method location, Type... types) { + this(pattern, location, null, types); + } + public StubStepDefinition(String pattern, Type... types) { this(pattern, STUBBED_LOCATION_WITH_DETAILS, null, types); } @@ -35,6 +41,14 @@ public StubStepDefinition(String pattern, String location, Throwable exception, this.exception = exception; } + + public StubStepDefinition(String pattern, Method location, Throwable exception, Type... types) { + this.parameterInfos = Stream.of(types).map(StubParameterInfo::new).collect(Collectors.toList()); + this.expression = pattern; + this.location = new StubLocation(location); + this.exception = exception; + } + @Override public boolean isDefinedAt(StackTraceElement stackTraceElement) { return false; @@ -45,6 +59,11 @@ public String getLocation() { return location.getLocation(); } + @Override + public Optional<SourceReference> getSourceReference() { + return location.getSourceReference(); + } + @Override public void execute(Object[] args) { if (exception != null) { diff --git a/cucumber-core/src/test/java/io/cucumber/core/plugin/HtmlFormatterTest.java b/cucumber-core/src/test/java/io/cucumber/core/plugin/HtmlFormatterTest.java index 1e5132b263..562e0361cd 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/plugin/HtmlFormatterTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/plugin/HtmlFormatterTest.java @@ -64,7 +64,7 @@ void ignores_step_definitions() throws Throwable { Hook hook = new Hook("", null, SourceReference.of("https://example.com"), - null); + null, null); bus.send(Envelope.of(hook)); // public ParameterType(String name, List<String> regularExpressions, diff --git a/cucumber-core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java b/cucumber-core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java index c96d7aca1a..f7842c05e1 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.StubHookDefinition; import io.cucumber.core.backend.StubStepDefinition; +import io.cucumber.core.eventbus.IncrementingUuidGenerator; import io.cucumber.core.feature.TestFeatureParser; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.options.RuntimeOptionsBuilder; @@ -18,6 +19,7 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.lang.reflect.Method; import java.util.Scanner; import java.util.UUID; @@ -33,6 +35,23 @@ class JsonFormatterTest { + final Method monkeyArrives = getMethod("monkey_arrives"); + final Method thereAreBananas = getMethod("there_are_bananas"); + final Method thereAreOranges = getMethod("there_are_oranges"); + final Method beforeHook1 = getMethod("before_hook_1"); + final Method afterHook1 = getMethod("after_hook_1"); + + final Method monkeyEatsBananas = getMethod("monkey_eats_bananas"); + final Method monkeyEatsMoreBananas = getMethod("monkey_eats_more_bananas"); + + private static Method getMethod(String name) { + try { + return StepDefs.class.getMethod(name); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + @Test void featureWithOutlineTest() throws JSONException { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -168,9 +187,9 @@ void should_format_scenario_with_a_passed_step() throws JSONException { Runtime.builder() .withFeatureSupplier(new StubFeatureSupplier(feature)) .withAdditionalPlugins(timeService, new JsonFormatter(out)) - .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) + .withEventBus(new TimeServiceEventBus(timeService, new IncrementingUuidGenerator())) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"))) + new StubStepDefinition("there are bananas", thereAreBananas))) .build() .run(); @@ -198,7 +217,7 @@ void should_format_scenario_with_a_passed_step() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -229,7 +248,7 @@ void should_format_scenario_with_a_failed_step() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()", + new StubStepDefinition("there are bananas", thereAreBananas, new StubException()))) .build() .run(); @@ -258,7 +277,7 @@ void should_format_scenario_with_a_failed_step() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"failed\",\n" + @@ -291,7 +310,7 @@ void should_format_scenario_with_a_rule() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"))) + new StubStepDefinition("there are bananas", thereAreBananas))) .build() .run(); @@ -305,7 +324,7 @@ void should_format_scenario_with_a_rule() throws JSONException { " \"line\": 4,\n" + " \"name\": \"Monkey eats bananas\",\n" + " \"description\": \"\",\n" + - " \"id\": \";monkey-eats-bananas\",\n" + + " \"id\": \"banana-party;this-is-all-monkey-business;monkey-eats-bananas\",\n" + " \"type\": \"scenario\",\n" + " \"keyword\": \"Scenario\",\n" + " \"steps\": [\n" + @@ -317,7 +336,7 @@ void should_format_scenario_with_a_rule() throws JSONException { " \"line\": 5,\n" + " \"name\": \"there are bananas\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"keyword\": \"Given \"\n" + " }\n" + @@ -358,7 +377,7 @@ void should_format_scenario_with_a_rule_and_background() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"))) + new StubStepDefinition("there are bananas", thereAreBananas))) .build() .run(); @@ -382,7 +401,7 @@ void should_format_scenario_with_a_rule_and_background() throws JSONException { " \"line\": 4,\n" + " \"name\": \"there are bananas\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"keyword\": \"Given \"\n" + " },\n" + @@ -394,7 +413,7 @@ void should_format_scenario_with_a_rule_and_background() throws JSONException { " \"line\": 9,\n" + " \"name\": \"there are bananas\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"keyword\": \"Given \"\n" + " }\n" + @@ -417,7 +436,7 @@ void should_format_scenario_with_a_rule_and_background() throws JSONException { " \"line\": 12,\n" + " \"name\": \"there are bananas\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"keyword\": \"Given \"\n" + " }\n" + @@ -453,7 +472,7 @@ void should_format_scenario_outline_with_one_example() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"))) + new StubStepDefinition("there are bananas", thereAreBananas))) .build() .run(); @@ -481,7 +500,7 @@ void should_format_scenario_outline_with_one_example() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -518,9 +537,9 @@ void should_format_feature_with_background() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"), - new StubStepDefinition("the monkey eats bananas", "StepDefs.monkey_eats_bananas()"), - new StubStepDefinition("the monkey eats more bananas", "StepDefs.monkey_eats_more_bananas()"))) + new StubStepDefinition("there are bananas", thereAreBananas), + new StubStepDefinition("the monkey eats bananas", monkeyEatsBananas), + new StubStepDefinition("the monkey eats more bananas", monkeyEatsMoreBananas))) .build() .run(); @@ -546,7 +565,7 @@ void should_format_feature_with_background() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -569,7 +588,7 @@ void should_format_feature_with_background() throws JSONException { " \"name\": \"the monkey eats bananas\",\n" + " \"line\": 7,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.monkey_eats_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#monkey_eats_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -590,7 +609,7 @@ void should_format_feature_with_background() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -613,7 +632,7 @@ void should_format_feature_with_background() throws JSONException { " \"name\": \"the monkey eats more bananas\",\n" + " \"line\": 10,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.monkey_eats_more_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#monkey_eats_more_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -645,7 +664,7 @@ void should_format_feature_and_scenario_with_tags() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("the monkey eats more bananas", "StepDefs.monkey_eats_more_bananas()"))) + new StubStepDefinition("the monkey eats more bananas", monkeyEatsMoreBananas))) .build() .run(); @@ -671,7 +690,7 @@ void should_format_feature_and_scenario_with_tags() throws JSONException { " \"line\": 5,\n" + " \"name\": \"the monkey eats more bananas\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.monkey_eats_more_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#monkey_eats_more_bananas()\"\n" + " },\n" + " \"keyword\": \"Then \"\n" + " }\n" + @@ -732,9 +751,9 @@ void should_format_scenario_with_hooks() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - singletonList(new StubHookDefinition("Hooks.before_hook_1()")), - singletonList(new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()")), - singletonList(new StubHookDefinition("Hooks.after_hook_1()")))) + singletonList(new StubHookDefinition(beforeHook1)), + singletonList(new StubStepDefinition("there are bananas", thereAreBananas)), + singletonList(new StubHookDefinition(afterHook1)))) .build() .run(); @@ -773,7 +792,7 @@ void should_format_scenario_with_hooks() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -819,8 +838,8 @@ void should_add_step_hooks_to_step() throws JSONException { emptyList(), singletonList(new StubHookDefinition("Hooks.beforestep_hooks_1()")), asList( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"), - new StubStepDefinition("monkey arrives", "StepDefs.monkey_arrives()")), + new StubStepDefinition("there are bananas", thereAreBananas), + new StubStepDefinition("monkey arrives", monkeyArrives)), asList( new StubHookDefinition("Hooks.afterstep_hooks_1()"), new StubHookDefinition("Hooks.afterstep_hooks_2()")), @@ -861,7 +880,7 @@ void should_add_step_hooks_to_step() throws JSONException { " \"line\": 4,\n" + " \"name\": \"there are bananas\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"after\": [\n" + " {\n" + @@ -904,7 +923,7 @@ void should_add_step_hooks_to_step() throws JSONException { " \"line\": 5,\n" + " \"name\": \"monkey arrives\",\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.monkey_arrives()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#monkey_arrives()\"\n" + " },\n" + " \"after\": [\n" + " {\n" + @@ -957,9 +976,9 @@ void should_handle_write_from_a_hook() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - singletonList(new StubHookDefinition("Hooks.before_hook_1()", + singletonList(new StubHookDefinition(beforeHook1, testCaseState -> testCaseState.log("printed from hook"))), - singletonList(new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()")), + singletonList(new StubStepDefinition("there are bananas", thereAreBananas)), emptyList())) .build() .run(); @@ -1002,7 +1021,7 @@ void should_handle_write_from_a_hook() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1036,7 +1055,7 @@ void should_handle_embed_from_a_hook() throws JSONException { singletonList(new StubHookDefinition("Hooks.before_hook_1()", testCaseState -> testCaseState .attach(new byte[] { 1, 2, 3 }, "mime-type;base64", null))), - singletonList(new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()")), + singletonList(new StubStepDefinition("there are bananas", thereAreBananas)), emptyList())) .build() .run(); @@ -1082,7 +1101,7 @@ void should_handle_embed_from_a_hook() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1116,7 +1135,7 @@ void should_handle_embed_with_name_from_a_hook() throws JSONException { singletonList(new StubHookDefinition("Hooks.before_hook_1()", testCaseState -> testCaseState.attach(new byte[] { 1, 2, 3 }, "mime-type;base64", "someEmbedding"))), - singletonList(new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()")), + singletonList(new StubStepDefinition("there are bananas", thereAreBananas)), emptyList())) .build() .run(); @@ -1163,7 +1182,7 @@ void should_handle_embed_with_name_from_a_hook() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1197,7 +1216,7 @@ void should_format_scenario_with_a_step_with_a_doc_string() throws JSONException .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()", String.class))) + new StubStepDefinition("there are bananas", thereAreBananas, String.class))) .build() .run(); @@ -1229,7 +1248,7 @@ void should_format_scenario_with_a_step_with_a_doc_string() throws JSONException " \"line\": 5\n" + " },\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1263,7 +1282,7 @@ void should_format_scenario_with_a_step_with_a_doc_string_and_content_type() thr .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()", DocString.class))) + new StubStepDefinition("there are bananas", thereAreBananas, DocString.class))) .build() .run(); @@ -1296,7 +1315,7 @@ void should_format_scenario_with_a_step_with_a_doc_string_and_content_type() thr " \"line\": 5\n" + " },\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1329,7 +1348,7 @@ void should_format_scenario_with_a_step_with_a_data_table() throws JSONException .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()", DataTable.class))) + new StubStepDefinition("there are bananas", thereAreBananas, DataTable.class))) .build() .run(); @@ -1371,7 +1390,7 @@ void should_format_scenario_with_a_step_with_a_data_table() throws JSONException " }\n" + " ],\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1407,8 +1426,8 @@ void should_handle_several_features() throws JSONException { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()"), - new StubStepDefinition("there are oranges", "StepDefs.there_are_oranges()"))) + new StubStepDefinition("there are bananas", thereAreBananas), + new StubStepDefinition("there are oranges", thereAreOranges))) .build() .run(); @@ -1436,7 +1455,7 @@ void should_handle_several_features() throws JSONException { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_bananas()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_bananas()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1470,7 +1489,7 @@ void should_handle_several_features() throws JSONException { " \"name\": \"there are oranges\",\n" + " \"line\": 4,\n" + " \"match\": {\n" + - " \"location\": \"StepDefs.there_are_oranges()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#there_are_oranges()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -1486,4 +1505,27 @@ void should_handle_several_features() throws JSONException { assertJsonEquals(expected, out); } + static class StepDefs { + public void before_hook_1(){ + + } + public void after_hook_1(){ + + } + public void there_are_bananas(){ + + } + public void there_are_oranges(){ + + } + public void monkey_eats_bananas(){ + + } + public void monkey_eats_more_bananas(){ + + } + public void monkey_arrives(){ + + } + } }