diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index bbf0ed356e..9b8d8794dc 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -19,7 +19,7 @@ 21.12.0 0.7.1 27.2.0 - 13.3.0 + 13.3.1-SNAPSHOT 6.1.2 0.3.1 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..b0e484679b --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/CucumberJvmJson.java @@ -0,0 +1,422 @@ +package io.cucumber.core.plugin; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * Object representation of cucumber-jvm.json + * 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 elements; + private final List tags; + + JvmFeature( + String uri, String id, Long line, String keyword, String name, String description, + List elements, List 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 getElements() { + return elements; + } + + public List 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 steps; + private final List before; + private final List after; + private final List tags; + + JvmElement( + String start_timestamp, Long line, String id, JvmElementType type, String keyword, String name, + String description, List steps, List before, List after, List 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 getSteps() { + return steps; + } + + public List getBefore() { + return before; + } + + public List getAfter() { + return after; + } + + public List 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 rows; + private final List before; + private final List after; + + JvmStep( + String keyword, Long line, JvmMatch match, String name, JvmResult result, JvmDocString doc_string, + List rows, List before, List after + ) { + 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; + this.before = before; + this.after = after; + } + + 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 getRows() { + return rows; + } + + public List getBefore() { + return before; + } + + public List getAfter() { + return after; + } + } + + static class JvmMatch { + private final String location; + private final List arguments; + + JvmMatch(String location, List arguments) { + this.location = location; + this.arguments = arguments; + } + + public String getLocation() { + return location; + } + + public List 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 cells; + + JvmDataTableRow(List cells) { + this.cells = requireNonNull(cells); + } + + public List getCells() { + return cells; + } + } + + static class JvmHook { + private final JvmMatch match; + private final JvmResult result; + private final List embeddings; + private final List output; + + JvmHook(JvmMatch match, JvmResult result, List embeddings, List output) { + this.match = requireNonNull(match); + this.result = requireNonNull(result); + this.embeddings = embeddings; + this.output = output; + } + + public JvmMatch getMatch() { + return match; + } + + public JvmResult getResult() { + return result; + } + + public List getEmbeddings() { + return embeddings; + } + + public List getOutput() { + return output; + } + } + + 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..ffc53e05b8 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/IdNamingVisitor.java @@ -0,0 +1,55 @@ +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.messages.types.TableRow; +import io.cucumber.query.LineageReducer; + +import java.util.ArrayList; +import java.util.List; + +import static io.cucumber.core.plugin.TestSourcesModel.convertToId; + +class IdNamingVisitor implements LineageReducer.Collector { + + private final List parts = new ArrayList<>(); + + @Override + public void add(Feature feature) { + parts.add(convertToId(feature.getName())); + } + + @Override + public void add(Rule rule) { + parts.add(convertToId(rule.getName())); + } + + @Override + public void add(Scenario scenario) { + parts.add(convertToId(scenario.getName())); + } + + @Override + public void add(Examples examples, int index) { + parts.add(convertToId(examples.getName())); + } + + @Override + public void add(TableRow example, int index) { + // json report uses base-1 indexing, and skips the first row + parts.add(String.valueOf(index + 2)); + } + + @Override + public void add(Pickle pickle) { + convertToId(pickle.getName()); + } + + @Override + public String finish() { + return String.join(";", parts); + } +} 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> featureMaps = new ArrayList<>(); - private final Map currentBeforeStepHookList = new HashMap<>(); - private final Writer writer; - private final TestSourcesModel testSources = new TestSourcesModel(); - private URI currentFeatureFile; - private List> currentElementsList; - private Map currentElementMap; - private Map currentTestCaseMap; - private List> currentStepsList; - private Map 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 currentFeatureMap = createFeatureMap(event.getTestCase()); - featureMaps.add(currentFeatureMap); - currentElementsList = (List>) 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>) 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>) 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 createFeatureMap(TestCase testCase) { - Map featureMap = new HashMap<>(); - featureMap.put("uri", TestSourcesModel.relativize(testCase.getUri())); - featureMap.put("elements", new ArrayList>()); - 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 json = new LinkedHashMap<>(); - json.put("name", tag.getName()); - json.put("type", "Tag"); - Map 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 createTestCase(TestCaseStarted event) { - Map 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>()); - if (!testCase.getTags().isEmpty()) { - List> tagList = new ArrayList<>(); - for (String tag : testCase.getTags()) { - Map tagMap = new HashMap<>(); - tagMap.put("name", tag); - tagList.add(tagMap); - } - testCaseMap.put("tags", tagList); - } - return testCaseMap; - } - - private Map createBackground(TestCase testCase) { - TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLocation().getLine()); - if (astNode != null) { - Background background = getBackgroundForTestCase(astNode).get(); - Map 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>()); - 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 createTestStep(PickleStepTestStep testStep) { - Map 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 createHookStep(HookTestStep hookTestStep) { - return new HashMap<>(); - } - - private void addHookStepToTestCaseMap(Map currentStepOrHookMap, HookType hookType) { - String hookName; - if (hookType == HookType.AFTER || hookType == HookType.AFTER_STEP) - hookName = after; - else - hookName = before; - - Map 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>()); - } - ((List>) mapToAddTo.get(hookName)).add(currentStepOrHookMap); - } - - private Map createMatchMap(TestStep step, Result result) { - Map matchMap = new HashMap<>(); - if (step instanceof PickleStepTestStep) { - PickleStepTestStep testStep = (PickleStepTestStep) step; - if (!testStep.getDefinitionArgument().isEmpty()) { - List> argumentList = new ArrayList<>(); - for (Argument argument : testStep.getDefinitionArgument()) { - Map 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 createResultMap(Result result) { - Map 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()); - } - ((List) currentStepOrHookMap.get("output")).add(text); - } - - private void addEmbeddingToHookMap(byte[] data, String mediaType, String name) { - if (!currentStepOrHookMap.containsKey("embeddings")) { - currentStepOrHookMap.put("embeddings", new ArrayList>()); - } - Map embedMap = createEmbeddingMap(data, mediaType, name); - ((List>) currentStepOrHookMap.get("embeddings")).add(embedMap); - } - - private Map createDummyFeatureForFailure(TestRunFinished event) { - Throwable exception = event.getResult().getError(); - - Map feature = new LinkedHashMap<>(); - feature.put("line", 1); - { - Map 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 when = new LinkedHashMap<>(); - Map then = new LinkedHashMap<>(); - scenario.put("steps", Arrays.asList(when, then)); - { - - { - Map 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 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 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 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 createDocStringMap(DocStringArgument docString) { - Map docStringMap = new HashMap<>(); - docStringMap.put("value", docString.getContent()); - docStringMap.put("line", docString.getLine()); - docStringMap.put("content_type", docString.getMediaType()); - return docStringMap; - } - - private List>> createDataTableList(DataTableArgument argument) { - List>> rowList = new ArrayList<>(); - for (List row : argument.cells()) { - Map> rowMap = new HashMap<>(); - rowMap.put("cells", new ArrayList<>(row)); - rowList.add(rowMap); - } - return rowList; - } - - private Map createEmbeddingMap(byte[] data, String mediaType, String name) { - Map 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> featureMaps = new ArrayList<>(); + private final Map currentBeforeStepHookList = new HashMap<>(); + private final Writer writer; + private final TestSourcesModel testSources = new TestSourcesModel(); + private URI currentFeatureFile; + private List> currentElementsList; + private Map currentElementMap; + private Map currentTestCaseMap; + private List> currentStepsList; + private Map 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 currentFeatureMap = createFeatureMap(event.getTestCase()); + featureMaps.add(currentFeatureMap); + currentElementsList = (List>) 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>) 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>) 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 createFeatureMap(TestCase testCase) { + Map featureMap = new HashMap<>(); + featureMap.put("uri", TestSourcesModel.relativize(testCase.getUri())); + featureMap.put("elements", new ArrayList>()); + 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 json = new LinkedHashMap<>(); + json.put("name", tag.getName()); + json.put("type", "Tag"); + Map 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 createTestCase(TestCaseStarted event) { + Map 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>()); + if (!testCase.getTags().isEmpty()) { + List> tagList = new ArrayList<>(); + for (String tag : testCase.getTags()) { + Map tagMap = new HashMap<>(); + tagMap.put("name", tag); + tagList.add(tagMap); + } + testCaseMap.put("tags", tagList); + } + return testCaseMap; + } + + private Map createBackground(TestCase testCase) { + TestSourcesModel.AstNode astNode = testSources.getAstNode(currentFeatureFile, testCase.getLocation().getLine()); + if (astNode != null) { + Background background = getBackgroundForTestCase(astNode).get(); + Map 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>()); + 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 createTestStep(PickleStepTestStep testStep) { + Map 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 createHookStep(HookTestStep hookTestStep) { + return new HashMap<>(); + } + + private void addHookStepToTestCaseMap(Map currentStepOrHookMap, HookType hookType) { + String hookName; + if (hookType == HookType.AFTER || hookType == HookType.AFTER_STEP) + hookName = after; + else + hookName = before; + + Map 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>()); + } + ((List>) mapToAddTo.get(hookName)).add(currentStepOrHookMap); + } + + private Map createMatchMap(TestStep step, Result result) { + Map matchMap = new HashMap<>(); + if (step instanceof PickleStepTestStep) { + PickleStepTestStep testStep = (PickleStepTestStep) step; + if (!testStep.getDefinitionArgument().isEmpty()) { + List> argumentList = new ArrayList<>(); + for (Argument argument : testStep.getDefinitionArgument()) { + Map 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 createResultMap(Result result) { + Map 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()); + } + ((List) currentStepOrHookMap.get("output")).add(text); + } + + private void addEmbeddingToHookMap(byte[] data, String mediaType, String name) { + if (!currentStepOrHookMap.containsKey("embeddings")) { + currentStepOrHookMap.put("embeddings", new ArrayList>()); + } + Map embedMap = createEmbeddingMap(data, mediaType, name); + ((List>) currentStepOrHookMap.get("embeddings")).add(embedMap); + } + + private Map createDummyFeatureForFailure(TestRunFinished event) { + Throwable exception = event.getResult().getError(); + + Map feature = new LinkedHashMap<>(); + feature.put("line", 1); + { + Map 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 when = new LinkedHashMap<>(); + Map then = new LinkedHashMap<>(); + scenario.put("steps", Arrays.asList(when, then)); + { + + { + Map 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 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 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 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 createDocStringMap(DocStringArgument docString) { + Map docStringMap = new HashMap<>(); + docStringMap.put("value", docString.getContent()); + docStringMap.put("line", docString.getLine()); + docStringMap.put("content_type", docString.getMediaType()); + return docStringMap; + } + + private List>> createDataTableList(DataTableArgument argument) { + List>> rowList = new ArrayList<>(); + for (List row : argument.cells()) { + Map> rowMap = new HashMap<>(); + rowMap.put("cells", new ArrayList<>(row)); + rowList.add(rowMap); + } + return rowList; + } + + private Map createEmbeddingMap(byte[] data, String mediaType, String name) { + Map 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..66ec24cc81 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/JsonReportWriter.java @@ -0,0 +1,456 @@ +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.Lineage; +import io.cucumber.query.LineageReducer; +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.AbstractMap.SimpleEntry; +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.AttachmentContentEncoding.BASE64; +import static io.cucumber.messages.types.AttachmentContentEncoding.IDENTITY; +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; + } + + private static JvmLocationTag createLocationTag(Tag tag) { + return new JvmLocationTag( + tag.getName(), + "Tag", + new JvmLocation( + tag.getLocation().getLine(), + tag.getLocation().getColumn().orElse(0L))); + } + + private static Optional findBackgroundBy(List backgrounds, PickleStep pickleStep) { + return backgrounds.stream() + .filter(background -> background.getSteps().stream() + .map(Step::getId) + .anyMatch(step -> pickleStep.getAstNodeIds().contains(step))) + .findFirst(); + } + + private static List createTags(Pickle pickle) { + return pickle.getTags().stream().map(pickleTag -> new JvmTag(pickleTag.getName())).collect(toList()); + } + + private static Predicate include(HookType... hookTypes) { + List keep = Arrays.asList(hookTypes); + return hook -> hook.getType().map(keep::contains).orElse(false); + } + + private static Predicate exclude(HookType... hookTypes) { + return include(hookTypes).negate(); + } + + 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())); + } + + List writeJsonReport() { + // TODO: Replace with findAllTestCaseStartedB + return query.findAllTestCaseStartedGroupedByFeature() + .entrySet() + .stream() + .map(this::createFeatureMap) + .collect(toList()); + } + + private JvmFeature createFeatureMap(Entry, List> 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 GherkinDocument getGherkinDocument(Entry, List> entry) { + return entry.getValue().stream() + .findFirst() + .flatMap(query::findLineageBy) + .map(Lineage::document) + .orElseThrow(() -> new IllegalArgumentException("No Gherkin document")); + } + + private List writeElementsReport(Entry, List> entry) { + return entry.getValue().stream() + .map(this::createTestCaseAndBackGround) + .flatMap(Collection::stream) + .collect(toList()); + } + + private List createTestCaseAndBackGround(TestCaseStarted testCaseStarted) { + // TODO: Clean up + Predicate, List>> isBackGround = entry -> entry.getKey() + .isPresent(); + Predicate, List>> isTestCase = isBackGround.negate(); + BinaryOperator, List>> mergeSteps = (a, b) -> { + a.getValue().addAll(b.getValue()); + return a; + }; + Map, List> 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 background = stepsByBackground.entrySet().stream() + .filter(isBackGround) + .reduce(mergeSteps) + .flatMap(entry -> entry.getKey().map(bg -> createBackground(bg, entry.getValue()))); + + Optional 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, ?, Map, List>> groupByBackground( + TestCaseStarted testCaseStarted + ) { + List backgrounds = query.findFeatureBy(testCaseStarted) + .map(this::getBackgroundsBy) + .orElseGet(Collections::emptyList); + + Function, Optional> grouping = entry -> query + .findPickleStepBy(entry.getValue()) + .flatMap(pickleStep -> findBackgroundBy(backgrounds, pickleStep)); + + Function>, List> extractKey = entries -> entries + .stream() + .map(Entry::getKey) + .collect(toList()); + + return groupingBy(grouping, LinkedHashMap::new, collectingAndThen(toList(), extractKey)); + } + + private List getBackgroundsBy(Feature feature) { + return feature.getChildren() + .stream() + .map(featureChild -> { + List 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 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 testStepsFinished) { + Pickle pickle = query.findPickleBy(event).get(); + Scenario scenario = query.findLineageBy(event).flatMap(Lineage::scenario).get(); + LineageReducer idStrategy = LineageReducer.descending(IdNamingVisitor::new); + + // TODO: Push down + List, TestStepFinished>> testStepsFinishedWithHookType = mapTestStepsFinishedToHookType(testStepsFinished); + List beforeHooks = createHookSteps(testStepsFinished, include(HookType.BEFORE_TEST_CASE)); + List afterHooks = createHookSteps(testStepsFinished, include(HookType.AFTER_TEST_CASE)); + return new JvmElement( + getDateTimeFromTimeStamp(event.getTimestamp()), + query.findLocationOf(pickle).get().getLine(), + query.findLineageBy(pickle).map(idStrategy::reduce).orElse(convertToId(pickle.getName())), + 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 createHookSteps( + List testStepsFinished, Predicate 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.findAttachmentsBy(testStepFinished)), + createOutput(query.findAttachmentsBy(testStepFinished)))))) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + + private List createEmbeddings(List attachments) { + if (attachments.isEmpty()) { + return null; + } + List embeddings = attachments.stream() + .filter(attachment -> attachment.getContentEncoding() == BASE64) + .map(attachment -> new CucumberJvmJson.JvmEmbedding( + attachment.getMediaType(), + attachment.getBody(), + attachment.getFileName().orElse(null))) + .collect(toList()); + + if (embeddings.isEmpty()) { + return null; + } + return embeddings; + } + + private List createOutput(List attachments) { + if (attachments.isEmpty()) { + return null; + } + List outputs = attachments.stream() + .filter(attachment -> attachment.getContentEncoding() == IDENTITY) + .map(Attachment::getBody) + .collect(toList()); + + if (outputs.isEmpty()) { + return null; + } + return outputs; + } + + private List createTestSteps(List testStepsFinished) { + return testStepsFinished.stream() + .map(this::createTestStep) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + + private List, TestStepFinished>> mapTestStepsFinishedToHookType(List testStepsFinished) { + return testStepsFinished.stream() + .map(testStepFinished -> new SimpleEntry<>(query.findTestStepBy(testStepFinished) + .flatMap(query::findHookBy) + .flatMap(Hook::getType), testStepFinished)) + .collect(toList()); + } + + private Optional 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), + // TODO: You are here + null, // createHookSteps(testStepsFinished, include(HookType.BEFORE_TEST_STEP)), + null // createHookSteps(testStepsFinished, include(HookType.AFTER_TEST_STEP)) + )))); + } + + private JvmMatch createMatchMap(TestStep step, TestStepResult result) { + Optional source = query.findStepDefinitionBy(step) + .stream() + .findFirst() + .map(StepDefinition::getSourceReference); + + Optional hookSource = query.findHookBy(step) + .map(Hook::getSourceReference); + + Optional location = Stream.of(source, hookSource) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst().flatMap(sourceReference -> { + Optional fromUri = sourceReference.getUri() + .map(uri -> renderLocationString(sourceReference, uri)); + + Optional fromJavaMethod = sourceReference.getJavaMethod() + .map(JsonReportWriter::renderLocationString); + + Optional fromStackTrace = sourceReference.getJavaStackTraceElement() + .map(javaStackTraceElement -> renderLocationString(sourceReference, javaStackTraceElement)); + + return Stream.of(fromStackTrace, fromJavaMethod, fromUri).filter(Optional::isPresent).map(Optional::get) + .findFirst(); + }); + + Optional> 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 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 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..bf0b4b2ea7 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/MessagesToJsonWriter.java @@ -0,0 +1,78 @@ +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. + *

+ * 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 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/test/java/io/cucumber/core/backend/StubHookDefinition.java b/cucumber-core/src/test/java/io/cucumber/core/backend/StubHookDefinition.java index 532e444942..af8cab27ba 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 @@ -6,49 +6,38 @@ public class StubHookDefinition implements HookDefinition { private static final String STUBBED_LOCATION_WITH_DETAILS = "{stubbed location with details}"; - private final String location; + private final Located location; private final RuntimeException exception; private final Consumer action; - private final SourceReference sourceReference; private final HookType hookType; public StubHookDefinition( - String location, RuntimeException exception, Consumer action, - SourceReference sourceReference, HookType hookType + Located location, RuntimeException exception, Consumer action, HookType hookType ) { this.location = location; this.exception = exception; this.action = action; - this.sourceReference = sourceReference; this.hookType = hookType; } - public StubHookDefinition(String location, Consumer action) { - this(location, null, action, null, null); - } - - public StubHookDefinition() { - this(STUBBED_LOCATION_WITH_DETAILS, null, null, null, null); + public StubHookDefinition(SourceReference location, HookType hookType, Consumer action) { + this(new StubLocation(location), null, action, hookType); } public StubHookDefinition(Consumer action) { - this(STUBBED_LOCATION_WITH_DETAILS, null, action, null, null); + this(new StubLocation(STUBBED_LOCATION_WITH_DETAILS), null, action, null); } public StubHookDefinition(RuntimeException exception) { - this(STUBBED_LOCATION_WITH_DETAILS, exception, null, null, null); - } - - public StubHookDefinition(String location) { - this(location, null, null, null, null); + this(new StubLocation(STUBBED_LOCATION_WITH_DETAILS), exception, null, null); } public StubHookDefinition(SourceReference sourceReference, HookType hookType) { - this(null, null, null, sourceReference, hookType); + this(new StubLocation(sourceReference), null, null, hookType); } public StubHookDefinition(SourceReference sourceReference, HookType hookType, RuntimeException exception) { - this(null, exception, null, sourceReference, hookType); + this(new StubLocation(sourceReference), exception, null, hookType); } @Override @@ -78,12 +67,12 @@ public boolean isDefinedAt(StackTraceElement stackTraceElement) { @Override public String getLocation() { - return location; + return location.getLocation(); } @Override public Optional getSourceReference() { - return Optional.ofNullable(sourceReference); + return location.getSourceReference(); } @Override 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..06964d63e7 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,26 @@ 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); + } + + public StubLocation(SourceReference sourceReference) { + this.location = null; + this.sourceReference = sourceReference; } @Override @@ -13,6 +28,11 @@ public boolean isDefinedAt(StackTraceElement stackTraceElement) { return false; } + @Override + public Optional 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..6cc5449401 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,14 @@ 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, SourceReference location, Type... types) { + this(pattern, location, null, types); + } + public StubStepDefinition(String pattern, Type... types) { this(pattern, STUBBED_LOCATION_WITH_DETAILS, null, types); } @@ -35,6 +45,20 @@ 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; + } + + public StubStepDefinition(String pattern, SourceReference 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 +69,11 @@ public String getLocation() { return location.getLocation(); } + @Override + public Optional 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/JsonFormatterTest.java b/cucumber-core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java index 28d2677e4e..bca370be7d 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 @@ -1,7 +1,10 @@ package io.cucumber.core.plugin; +import io.cucumber.core.backend.HookDefinition; +import io.cucumber.core.backend.SourceReference; 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; @@ -21,6 +24,9 @@ import java.util.Scanner; import java.util.UUID; +import static io.cucumber.core.backend.HookDefinition.HookType.AFTER_STEP; +import static io.cucumber.core.backend.HookDefinition.HookType.BEFORE; +import static io.cucumber.core.backend.HookDefinition.HookType.BEFORE_STEP; import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.Clock.fixed; import static java.time.Duration.ofMillis; @@ -33,6 +39,26 @@ class JsonFormatterTest { + final SourceReference monkeyArrives = getMethod("monkey_arrives"); + final SourceReference thereAreBananas = getMethod("there_are_bananas"); + final SourceReference thereAreOranges = getMethod("there_are_oranges"); + final SourceReference beforeHook1 = getMethod("before_hook_1"); + final SourceReference afterHook1 = getMethod("after_hook_1"); + final SourceReference beforeStepHook1 = getMethod("beforestep_hook_1"); + final SourceReference afterStepHook1 = getMethod("afterstep_hook_1"); + final SourceReference afterStepHook2 = getMethod("afterstep_hook_2"); + + final SourceReference monkeyEatsBananas = getMethod("monkey_eats_bananas"); + final SourceReference monkeyEatsMoreBananas = getMethod("monkey_eats_more_bananas"); + + private static SourceReference getMethod(String name) { + try { + return SourceReference.fromMethod(StepDefs.class.getMethod(name)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + @Test void featureWithOutlineTest() throws JSONException { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -57,7 +83,7 @@ private Builder createRuntime(ByteArrayOutputStream out) { .withFeatureSupplier(new StubFeatureSupplier(feature)) .withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - singletonList(new StubHookDefinition()), + singletonList(new StubHookDefinition(beforeHook1, BEFORE)), asList( new StubStepDefinition("bg_1"), new StubStepDefinition("bg_2"), @@ -168,9 +194,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 +224,8 @@ 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 +256,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("the stack trace")))) .build() .run(); @@ -258,7 +285,8 @@ 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 +319,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 +333,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 +345,8 @@ 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 +387,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 +411,8 @@ 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 +424,8 @@ 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" + @@ -405,7 +436,7 @@ void should_format_scenario_with_a_rule_and_background() throws JSONException { " \"line\": 11,\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" + @@ -417,7 +448,8 @@ 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 +485,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 +513,8 @@ 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 +551,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 +579,8 @@ 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 +603,8 @@ 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 +625,8 @@ 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 +649,8 @@ 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 +682,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 +708,8 @@ 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 +770,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, HookDefinition.HookType.BEFORE)), + singletonList(new StubStepDefinition("there are bananas", thereAreBananas)), + singletonList(new StubHookDefinition(afterHook1, HookDefinition.HookType.BEFORE)))) .build() .run(); @@ -759,7 +797,7 @@ void should_format_scenario_with_hooks() throws JSONException { " \"before\": [\n" + " {\n" + " \"match\": {\n" + - " \"location\": \"Hooks.before_hook_1()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()\"\n" + " },\n" + " \"result\": {\n" + " \"status\": \"passed\",\n" + @@ -773,7 +811,8 @@ 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" + @@ -817,13 +856,13 @@ void should_add_step_hooks_to_step() throws JSONException { .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( emptyList(), - singletonList(new StubHookDefinition("Hooks.beforestep_hooks_1()")), + singletonList(new StubHookDefinition(beforeStepHook1, BEFORE_STEP)), 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()")), + new StubHookDefinition(afterStepHook1, AFTER_STEP), + new StubHookDefinition(afterStepHook2, AFTER_STEP)), emptyList())) .build() .run(); @@ -861,7 +900,8 @@ 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 +944,8 @@ 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 +998,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, BEFORE, 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(); @@ -985,7 +1026,7 @@ void should_handle_write_from_a_hook() throws JSONException { " \"before\": [\n" + " {\n" + " \"match\": {\n" + - " \"location\": \"Hooks.before_hook_1()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()\"\n" + " },\n" + " \"output\": [\n" + " \"printed from hook\"\n" + @@ -1002,7 +1043,8 @@ 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" + @@ -1033,10 +1075,11 @@ void should_handle_embed_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, + BEFORE, 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(); @@ -1062,7 +1105,7 @@ void should_handle_embed_from_a_hook() throws JSONException { " \"before\": [\n" + " {\n" + " \"match\": {\n" + - " \"location\": \"Hooks.before_hook_1()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()\"\n" + " },\n" + " \"embeddings\": [\n" + " {\n" + @@ -1082,7 +1125,8 @@ 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" + @@ -1113,10 +1157,10 @@ void should_handle_embed_with_name_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, BEFORE, 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(); @@ -1142,7 +1186,7 @@ void should_handle_embed_with_name_from_a_hook() throws JSONException { " \"before\": [\n" + " {\n" + " \"match\": {\n" + - " \"location\": \"Hooks.before_hook_1()\"\n" + + " \"location\": \"io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()\"\n" + " },\n" + " \"embeddings\": [\n" + " {\n" + @@ -1163,7 +1207,8 @@ 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 +1242,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 +1274,8 @@ 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 +1309,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 +1342,8 @@ 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 +1376,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 +1418,8 @@ 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 +1455,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 +1484,8 @@ 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 +1519,8 @@ 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" + @@ -1485,4 +1535,43 @@ 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 beforestep_hook_1() { + + } + public void afterstep_hook_1() { + + } + public void afterstep_hook_2() { + + } + 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() { + + } + } } diff --git a/cucumber-core/src/test/resources/io/cucumber/core/plugin/JsonPrettyFormatterTest.json b/cucumber-core/src/test/resources/io/cucumber/core/plugin/JsonPrettyFormatterTest.json index b86942ff95..f05bb93a84 100644 --- a/cucumber-core/src/test/resources/io/cucumber/core/plugin/JsonPrettyFormatterTest.json +++ b/cucumber-core/src/test/resources/io/cucumber/core/plugin/JsonPrettyFormatterTest.json @@ -52,7 +52,7 @@ "status": "passed" }, "match": { - "location": "{stubbed location with details}" + "location": "io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()" } } ], @@ -159,7 +159,7 @@ "status": "passed" }, "match": { - "location": "{stubbed location with details}" + "location": "io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()" } } ], @@ -273,7 +273,7 @@ "status": "passed" }, "match": { - "location": "{stubbed location with details}" + "location": "io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()" } } ], @@ -387,7 +387,7 @@ "status": "passed" }, "match": { - "location": "{stubbed location with details}" + "location": "io.cucumber.core.plugin.JsonFormatterTest$StepDefs#before_hook_1()" } } ],