From 69940b95082548c886c555436e3ce20a76f1ea5a Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 10 Oct 2025 18:02:10 +0200 Subject: [PATCH 01/13] Initial implementation --- .github/renovate.json | 7 + .github/workflows/release-github.yaml | 18 + .github/workflows/release-mvn.yml | 25 + .github/workflows/test-java.yml | 41 + .github/workflows/test-testdata.yml | 36 + .gitignore | 2 + CHANGELOG.md | 12 + README.md | 18 +- java/.gitignore | 4 + java/pom.xml | 106 ++ .../io/cucumber/usageformatter/Durations.java | 44 + .../usageformatter/MessagesToUsageWriter.java | 114 +++ .../usageformatter/PlainTextSerializer.java | 151 +++ .../SourceReferenceFormatter.java | 42 + .../cucumber/usageformatter/UsageReport.java | 106 ++ .../usageformatter/UsageReportBuilder.java | 111 ++ .../usageformatter/DurationsTest.java | 57 ++ .../io/cucumber/usageformatter/Jackson.java | 43 + .../MessagesToUsageWriterAcceptanceTest.java | 117 +++ testdata/.gitignore | 1 + testdata/README.md | 16 + testdata/package-lock.json | 965 ++++++++++++++++++ testdata/package.json | 15 + testdata/src/ambiguous.ndjson | 13 + testdata/src/attachments.ndjson | 61 ++ testdata/src/backgrounds.ndjson | 36 + testdata/src/cdata.ndjson | 12 + testdata/src/data-tables.ndjson | 15 + testdata/src/doc-strings.ndjson | 24 + testdata/src/empty.ndjson | 9 + .../src/examples-tables-attachment.ndjson | 21 + testdata/src/examples-tables-undefined.ndjson | 41 + testdata/src/examples-tables.ndjson | 80 ++ .../src/global-hooks-afterall-error.ndjson | 27 + testdata/src/global-hooks-attachments.ndjson | 20 + .../src/global-hooks-beforeall-error.ndjson | 22 + testdata/src/global-hooks.ndjson | 31 + testdata/src/hooks-attachment.ndjson | 20 + testdata/src/hooks-conditional.ndjson | 36 + testdata/src/hooks-named.ndjson | 18 + testdata/src/hooks-undefined.ndjson | 18 + testdata/src/hooks.ndjson | 29 + testdata/src/markdown.ndjson | 35 + testdata/src/minimal.json | 20 + testdata/src/minimal.ndjson | 12 + testdata/src/minimal.plain.txt | 4 + .../src/multiple-features-reversed.ndjson | 64 ++ testdata/src/multiple-features.json | 60 ++ testdata/src/multiple-features.ndjson | 64 ++ testdata/src/multiple-features.plain.txt | 9 + testdata/src/parameter-types.ndjson | 13 + testdata/src/pending.ndjson | 30 + testdata/src/regular-expression.ndjson | 16 + testdata/src/retry-ambiguous.ndjson | 13 + testdata/src/retry-pending.ndjson | 12 + testdata/src/retry-undefined.ndjson | 12 + testdata/src/retry.ndjson | 53 + testdata/src/rules-backgrounds.ndjson | 44 + testdata/src/rules.ndjson | 47 + testdata/src/skipped.ndjson | 33 + testdata/src/stack-traces.ndjson | 12 + testdata/src/undefined.ndjson | 39 + testdata/src/unknown-parameter-type.ndjson | 13 + testdata/src/unused-steps.json | 25 + testdata/src/unused-steps.ndjson | 13 + testdata/src/unused-steps.plain.txt | 6 + 66 files changed, 3231 insertions(+), 2 deletions(-) create mode 100644 .github/renovate.json create mode 100644 .github/workflows/release-github.yaml create mode 100644 .github/workflows/release-mvn.yml create mode 100644 .github/workflows/test-java.yml create mode 100644 .github/workflows/test-testdata.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 java/.gitignore create mode 100644 java/pom.xml create mode 100644 java/src/main/java/io/cucumber/usageformatter/Durations.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/SourceReferenceFormatter.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/UsageReport.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java create mode 100644 java/src/test/java/io/cucumber/usageformatter/DurationsTest.java create mode 100644 java/src/test/java/io/cucumber/usageformatter/Jackson.java create mode 100644 java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java create mode 100644 testdata/.gitignore create mode 100644 testdata/README.md create mode 100644 testdata/package-lock.json create mode 100644 testdata/package.json create mode 100644 testdata/src/ambiguous.ndjson create mode 100644 testdata/src/attachments.ndjson create mode 100644 testdata/src/backgrounds.ndjson create mode 100644 testdata/src/cdata.ndjson create mode 100644 testdata/src/data-tables.ndjson create mode 100644 testdata/src/doc-strings.ndjson create mode 100644 testdata/src/empty.ndjson create mode 100644 testdata/src/examples-tables-attachment.ndjson create mode 100644 testdata/src/examples-tables-undefined.ndjson create mode 100644 testdata/src/examples-tables.ndjson create mode 100644 testdata/src/global-hooks-afterall-error.ndjson create mode 100644 testdata/src/global-hooks-attachments.ndjson create mode 100644 testdata/src/global-hooks-beforeall-error.ndjson create mode 100644 testdata/src/global-hooks.ndjson create mode 100644 testdata/src/hooks-attachment.ndjson create mode 100644 testdata/src/hooks-conditional.ndjson create mode 100644 testdata/src/hooks-named.ndjson create mode 100644 testdata/src/hooks-undefined.ndjson create mode 100644 testdata/src/hooks.ndjson create mode 100644 testdata/src/markdown.ndjson create mode 100644 testdata/src/minimal.json create mode 100644 testdata/src/minimal.ndjson create mode 100644 testdata/src/minimal.plain.txt create mode 100644 testdata/src/multiple-features-reversed.ndjson create mode 100644 testdata/src/multiple-features.json create mode 100644 testdata/src/multiple-features.ndjson create mode 100644 testdata/src/multiple-features.plain.txt create mode 100644 testdata/src/parameter-types.ndjson create mode 100644 testdata/src/pending.ndjson create mode 100644 testdata/src/regular-expression.ndjson create mode 100644 testdata/src/retry-ambiguous.ndjson create mode 100644 testdata/src/retry-pending.ndjson create mode 100644 testdata/src/retry-undefined.ndjson create mode 100644 testdata/src/retry.ndjson create mode 100644 testdata/src/rules-backgrounds.ndjson create mode 100644 testdata/src/rules.ndjson create mode 100644 testdata/src/skipped.ndjson create mode 100644 testdata/src/stack-traces.ndjson create mode 100644 testdata/src/undefined.ndjson create mode 100644 testdata/src/unknown-parameter-type.ndjson create mode 100644 testdata/src/unused-steps.json create mode 100644 testdata/src/unused-steps.ndjson create mode 100644 testdata/src/unused-steps.plain.txt diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..782e40a --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>cucumber/renovate-config", + "github>cucumber/renovate-config:messages-range-strategy-widen" + ] +} diff --git a/.github/workflows/release-github.yaml b/.github/workflows/release-github.yaml new file mode 100644 index 0000000..f991505 --- /dev/null +++ b/.github/workflows/release-github.yaml @@ -0,0 +1,18 @@ +name: Release GitHub + +on: + push: + branches: [release/*] + +jobs: + create-github-release: + name: Create GitHub Release and Git tag + runs-on: ubuntu-latest + environment: Release + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + - uses: cucumber/action-create-github-release@v1.1.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-mvn.yml b/.github/workflows/release-mvn.yml new file mode 100644 index 0000000..36f9622 --- /dev/null +++ b/.github/workflows/release-mvn.yml @@ -0,0 +1,25 @@ +name: Release Maven + +on: + push: + branches: [release/*] + +jobs: + publish-mvn: + name: Publish Maven Package + runs-on: ubuntu-latest + environment: Release + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' + - uses: cucumber/action-publish-mvn@v3.0.0 + with: + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} + nexus-username: ${{ secrets.SONATYPE_USERNAME }} + nexus-password: ${{ secrets.SONATYPE_PASSWORD }} + working-directory: java diff --git a/.github/workflows/test-java.yml b/.github/workflows/test-java.yml new file mode 100644 index 0000000..8fdc24c --- /dev/null +++ b/.github/workflows/test-java.yml @@ -0,0 +1,41 @@ +name: test-java + +on: + push: + branches: + - main + - renovate/** + paths: + - java/** + - testdata/** + - .github/** + pull_request: + branches: + - main + paths: + - java/** + - testdata/** + - .github/** + workflow_call: + +jobs: + test-java: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + java: ['17', '21'] + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + + - run: mvn verify + working-directory: java diff --git a/.github/workflows/test-testdata.yml b/.github/workflows/test-testdata.yml new file mode 100644 index 0000000..ac81e2e --- /dev/null +++ b/.github/workflows/test-testdata.yml @@ -0,0 +1,36 @@ +name: test-testdata + +on: + push: + branches: + - main + paths: + - testdata/** + pull_request: + branches: + - main + paths: + - testdata/** + +jobs: + test-testdata: + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + cache: 'npm' + cache-dependency-path: testdata/package-lock.json + + - run: npm ci + working-directory: testdata + + - name: check repository is not dirty + run: "[[ -z $(git status --porcelain) ]]" + + - name: show diff + if: ${{ failure() }} + run: git status --porcelain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1062418 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +*.iml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..38c2312 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The formatter is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added +- Java implementation ([#1](https://github.com/cucumber/pretty-formatter/pull/1) M.P. Korstanje) + +[Unreleased]: https://github.com/cucumber/pretty-formatter/compare/f21831df98c5e57a53950a92b068df8321e45bce...HEAD diff --git a/README.md b/README.md index 261b893..58c0314 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -# usage-formatter -Prints usage statatistics for step definitions +# Usage Formatter +⚠️ This is an internal package; you don't need to install it in order to use the JSON Formatter. + +[![Maven Central](https://img.shields.io/maven-central/v/io.cucumber/cucumber-json-formatter.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:io.cucumber%20AND%20a:cucumber-json-formatter) + +Writes usage statistics for step definitions + +## Features + +TODO: + +## Contributing + +Each language implementation validates itself against the examples in the +`testdata` folder. See the [testdata/README.md](testdata/README.md) for more +information. diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 0000000..6d8ce41 --- /dev/null +++ b/java/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml +target/ +pom.xml.versionsBackup diff --git a/java/pom.xml b/java/pom.xml new file mode 100644 index 0000000..4baf088 --- /dev/null +++ b/java/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + + io.cucumber + cucumber-parent + 4.4.0 + + + pretty-formatter + 0.0.1-SNAPSHOT + jar + Usage Formatter + Writes usage statistics for step definitions + https://github.com/cucumber/usage-formatter + + + io.cucumber.usageformatter + 1758245480 + + + + scm:git:git://github.com/cucumber/usage-formatter.git + scm:git:git@github.com:cucumber/usage-formatter.git + git://github.com/cucumber/usage-formatter.git + HEAD + + + + + + org.junit + junit-bom + 6.0.0 + pom + import + + + + com.fasterxml.jackson + jackson-bom + 2.20.0 + pom + import + + + + org.assertj + assertj-bom + 3.27.6 + pom + import + + + + + + + io.cucumber + messages + [29.0.1,31.0.0) + + + io.cucumber + query + [14.3.0,15.0.0) + + + + com.fasterxml.jackson.core + jackson-databind + test + + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + test + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + test + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter + test + + + diff --git a/java/src/main/java/io/cucumber/usageformatter/Durations.java b/java/src/main/java/io/cucumber/usageformatter/Durations.java new file mode 100644 index 0000000..976f8cb --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/Durations.java @@ -0,0 +1,44 @@ +package io.cucumber.usageformatter; + +import java.math.BigDecimal; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +final class Durations { + static UsageReport.Statistics createStatistics(List durations) { + if (durations.isEmpty()) { + return null; + } + + Duration sum = durations.stream() + .reduce(Duration::plus) + // Can't happen + .orElse(Duration.ZERO); + Duration mean = sum.dividedBy(durations.size()); + Duration moe95 = calculateMarginOfError95(durations, mean); + return new UsageReport.Statistics(sum, mean, moe95); + } + + /** + * Calculate the margin of error with a 0.95% confidence interval. + */ + private static Duration calculateMarginOfError95(List durations, Duration mean) { + BigDecimal meanSeconds = toBigDecimalSeconds(mean); + BigDecimal variance = durations.stream() + .map(Durations::toBigDecimalSeconds) + .map(durationSeconds -> durationSeconds.subtract(meanSeconds).pow(2)) + .reduce(BigDecimal::add) + .orElse(BigDecimal.ZERO); + // TODO: With Java 17, use BigDecimal.sqrt and + // BigDecimal.divideAndRemainder for seconds and nos + double marginOfError = 2 * Math.sqrt(variance.doubleValue()) / durations.size(); + long seconds = (long) Math.floor(marginOfError); + long nanos = (long) Math.floor((marginOfError - seconds) * TimeUnit.SECONDS.toNanos(1)); + return Duration.ofSeconds(seconds, nanos); + } + + static BigDecimal toBigDecimalSeconds(Duration duration) { + return BigDecimal.valueOf(duration.getSeconds()).add(BigDecimal.valueOf(duration.getNano(), 9)); + } +} diff --git a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java new file mode 100644 index 0000000..595632d --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java @@ -0,0 +1,114 @@ +package io.cucumber.usageformatter; + +import io.cucumber.messages.types.Envelope; +import io.cucumber.query.Query; +import io.cucumber.query.Repository; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +import static io.cucumber.query.Repository.RepositoryFeature.INCLUDE_GHERKIN_DOCUMENTS; +import static io.cucumber.query.Repository.RepositoryFeature.INCLUDE_STEP_DEFINITIONS; +import static java.util.Objects.requireNonNull; + +/** + * Creates a usage report for step definitions based on a test run. + *

+ * Note: Messages are first collected and only written once the stream is + * closed. + */ +public final class MessagesToUsageWriter implements AutoCloseable { + + private final OutputStreamWriter out; + private final Repository repository = Repository.builder() + .feature(INCLUDE_GHERKIN_DOCUMENTS, true) + .feature(INCLUDE_STEP_DEFINITIONS, true) + .build(); + private final Query query = new Query(repository); + private final Serializer serializer; + private final Function uriFormatter; + private boolean streamClosed = false; + + MessagesToUsageWriter(OutputStream out, Serializer serializer, Function uriFormatter) { + this.out = new OutputStreamWriter( + requireNonNull(out), + StandardCharsets.UTF_8); + this.serializer = requireNonNull(serializer); + this.uriFormatter = requireNonNull(uriFormatter); + } + + public void write(Envelope envelope) throws IOException { + if (streamClosed) { + throw new IOException("Stream closed"); + } + repository.update(envelope); + } + + public static Builder builder(Serializer serializer) { + return new Builder(serializer); + } + + public static final class Builder { + private final Serializer serializer; + private Function uriFormatter = Function.identity(); + + private Builder(Serializer serializer) { + this.serializer = requireNonNull(serializer); + } + + private static Function removePrefix(String prefix) { + // TODO: Needs coverage + return s -> { + if (s.startsWith(prefix)) { + return s.substring(prefix.length()); + } + return s; + }; + } + + /** + * Removes a given prefix from all URI locations. + *

+ * The typical usage would be to trim the current working directory. + * This makes the report more readable. + */ + public Builder removeUriPrefix(String prefix) { + // TODO: Needs coverage + this.uriFormatter = removePrefix(requireNonNull(prefix)); + return this; + } + + public MessagesToUsageWriter build(OutputStream out) { + requireNonNull(out); + return new MessagesToUsageWriter(out, serializer, uriFormatter); + } + } + + @Override + public void close() throws IOException { + if (streamClosed) { + return; + } + try { + UsageReport report = new UsageReportBuilder(query, uriFormatter).build(); + serializer.writeValue(out, report); + } finally { + try { + out.close(); + } finally { + streamClosed = true; + } + } + } + + @FunctionalInterface + public interface Serializer { + + void writeValue(Writer writer, UsageReport value) throws IOException; + + } +} diff --git a/java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java b/java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java new file mode 100644 index 0000000..c98ca91 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java @@ -0,0 +1,151 @@ +package io.cucumber.usageformatter; + +import io.cucumber.usageformatter.UsageReport.Statistics; +import io.cucumber.usageformatter.UsageReport.StepDefinitionUsage; + +import java.io.IOException; +import java.io.Writer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; +import static java.lang.System.lineSeparator; +import static java.math.RoundingMode.HALF_EVEN; +import static java.util.Comparator.comparing; +import static java.util.Comparator.nullsFirst; + +public final class PlainTextSerializer implements MessagesToUsageWriter.Serializer { + + // TODO: Make inclusion of steps configurable + // TODO: Make configurable? + public static final int MAX_NUMBER_OF_STEPS = 5; + public static final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; + public static final boolean[] leftAlignHeader = {true, false, false, true, false, true}; + + // TODO: Builder + PlainTextSerializer() { + + } + + @Override + public void writeValue(Writer writer, UsageReport value) throws IOException { + writer.append(format(value)); + } + + private static String format(UsageReport usageReport) { + List stepDefinitions = usageReport.getStepDefinitions(); + + if (stepDefinitions.isEmpty()) { + return ""; + } + + List table = new ArrayList<>(); + table.add(headers); + + stepDefinitions + .stream() + .sorted( + comparing(StepDefinitionUsage::getDuration, nullsFirst(comparing(Statistics::getMean))).reversed()) + .forEach(stepDefinitionUsage -> { + Statistics duration = stepDefinitionUsage.getDuration(); + table.add(new String[]{ + stepDefinitionUsage.getExpression(), + duration == null ? "" : formatDuration(duration.getSum()), + duration == null ? "" : formatDuration(duration.getMean()), + duration == null ? "" : "±", + duration == null ? "" : formatDuration(duration.getMoe95()), + stepDefinitionUsage.getLocation() + }); + + if (stepDefinitionUsage.getSteps().isEmpty()) { + table.add(new String[]{ + " UNUSED", + "", + "", + "", + "", + "" + }); + } else { + stepDefinitionUsage.getSteps().stream() + .sorted(comparing(UsageReport.StepUsage::getDuration).reversed()) + .limit(5) + .forEach(stepUsage -> + table.add(new String[]{ + " " + stepUsage.getText(), + formatDuration(stepUsage.getDuration()), + "", + "", + "", + stepUsage.getLocation() + })); + if (stepDefinitionUsage.getSteps().size() > MAX_NUMBER_OF_STEPS) { + table.add(new String[]{ + " " + (stepDefinitionUsage.getSteps().size() - MAX_NUMBER_OF_STEPS) + " more", + "", + "", + "", + "", + "" + }); + } + } + + }); + + StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); + int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); + for (String[] row : table) { + StringJoiner rowJoiner = new StringJoiner(" "); + for (int j = 0; j < row.length; j++) { + String newElement = renderCellWithPadding( + longestCellLengthInColumn[j], + row[j], + leftAlignHeader[j] + ); + rowJoiner.add(newElement); + } + joiner.add(rowJoiner.toString()); + } + return joiner.toString(); + } + + private static String formatDuration(Duration duration) { + return toBigDecimalSeconds(duration).setScale(3, HALF_EVEN).toPlainString() + "s"; + } + + private static int[] findLongestCellLengthInColumn(List renderedCells) { + // always square and non-sparse. + int width = renderedCells.get(0).length; + int[] longestCellInColumnLength = new int[width]; + for (String[] row : renderedCells) { + for (int colIndex = 0; colIndex < width; colIndex++) { + int current = longestCellInColumnLength[colIndex]; + int candidate = row[colIndex].length(); + longestCellInColumnLength[colIndex] = Math.max(current, candidate); + } + } + return longestCellInColumnLength; + } + + private static String renderCellWithPadding(int width, String cell, boolean leftAlign) { + StringBuilder result = new StringBuilder(); + if (leftAlign) { + result.append(cell); + padSpace(result, width - cell.length()); + } else { + padSpace(result, width - cell.length()); + result.append(cell); + } + return result.toString(); + } + + private static void padSpace(StringBuilder result, int padding) { + for (int i = 0; i < padding; i++) { + result.append(" "); + } + } + +} diff --git a/java/src/main/java/io/cucumber/usageformatter/SourceReferenceFormatter.java b/java/src/main/java/io/cucumber/usageformatter/SourceReferenceFormatter.java new file mode 100644 index 0000000..130e3f0 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/SourceReferenceFormatter.java @@ -0,0 +1,42 @@ +package io.cucumber.usageformatter; + +import io.cucumber.messages.types.Location; +import io.cucumber.messages.types.SourceReference; + +import java.util.Optional; +import java.util.function.Function; + +final class SourceReferenceFormatter { + private final Function uriFormatter; + + SourceReferenceFormatter(Function uriFormatter) { + this.uriFormatter = uriFormatter; + } + + Optional format(SourceReference sourceReference) { + if (sourceReference.getJavaMethod().isPresent()) { + return sourceReference.getJavaMethod() + .map(javaMethod -> String.format( + "%s.%s(%s)", + javaMethod.getClassName(), + javaMethod.getMethodName(), + String.join(",", javaMethod.getMethodParameterTypes()))); + } + if (sourceReference.getJavaStackTraceElement().isPresent()) { + return sourceReference.getJavaStackTraceElement() + .map(javaStackTraceElement -> String.format( + "%s.%s(%s%s)", + javaStackTraceElement.getClassName(), + javaStackTraceElement.getMethodName(), + javaStackTraceElement.getFileName(), + sourceReference.getLocation().map(Location::getLine).map(line -> ":" + line).orElse(""))); + } + if (sourceReference.getUri().isPresent()) { + return sourceReference.getUri() + .map(uri -> uriFormatter.apply(uri) + sourceReference.getLocation() + .map(location -> ":" + location.getLine()) + .orElse("")); + } + return Optional.empty(); + } +} diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java new file mode 100644 index 0000000..5ff03d8 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java @@ -0,0 +1,106 @@ +package io.cucumber.usageformatter; + +import java.time.Duration; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public final class UsageReport { + private final List stepDefinitions; + + UsageReport(List stepDefinitions) { + this.stepDefinitions = requireNonNull(stepDefinitions); + } + + public List getStepDefinitions() { + return stepDefinitions; + } + + /** + * Container for usage-entries of steps + */ + public static final class StepDefinitionUsage { + + private final String expression; + private final String location; + private final Statistics duration; + private final List steps; + + StepDefinitionUsage( + String expression, String location, Statistics duration, List steps + ) { + this.expression = requireNonNull(expression); + this.location = requireNonNull(location); + this.duration = duration; + this.steps = requireNonNull(steps); + } + + public String getExpression() { + return expression; + } + + public Statistics getDuration() { + return duration; + } + + public List getSteps() { + return steps; + } + + public String getLocation() { + return location; + } + } + + public static final class Statistics { + private final Duration sum; + private final Duration mean; + private final Duration moe95; + + Statistics(Duration sum, Duration mean, Duration moe95) { + this.sum = sum; + this.mean = mean; + this.moe95 = moe95; + } + + public Duration getSum() { + return sum; + } + + public Duration getMean() { + return mean; + } + + /** + * Margin of error with a 95% confidence interval, assuming a normal distribution. + */ + public Duration getMoe95() { + return moe95; + } + } + + public static final class StepUsage { + + private final String text; + private final Duration duration; + private final String location; + + StepUsage(String text, Duration duration, String location) { + this.text = requireNonNull(text); + this.duration = requireNonNull(duration); + this.location = requireNonNull(location); + } + + public Duration getDuration() { + return duration; + } + + public String getLocation() { + return location; + } + + public String getText() { + return text; + } + } +} diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java new file mode 100644 index 0000000..6a37c79 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java @@ -0,0 +1,111 @@ +package io.cucumber.usageformatter; + +import io.cucumber.messages.Convertor; +import io.cucumber.messages.types.Location; +import io.cucumber.messages.types.PickleStep; +import io.cucumber.messages.types.StepDefinition; +import io.cucumber.messages.types.TestStepFinished; +import io.cucumber.query.Query; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; + +final class UsageReportBuilder { + + private final Query query; + private final Function uriFormatter; + private final SourceReferenceFormatter sourceReferenceFormatter; + + UsageReportBuilder(Query query, Function uriFormatter) { + this.query = requireNonNull(query); + this.uriFormatter = requireNonNull(uriFormatter); + this.sourceReferenceFormatter = new SourceReferenceFormatter(uriFormatter); + } + + UsageReport build() { + Map, List> testStepsFinishedByStepDefinition = query + .findAllTestStepFinished() + .stream() + .collect(groupingBy(findUnambiguousStepDefinitionBy(), LinkedHashMap::new, + mapping(createStepUsage(), toList()))); + + // Add unused step definitions + query.findAllStepDefinitions().stream() + .map(Optional::of) + .forEach(stepDefinition -> testStepsFinishedByStepDefinition + .computeIfAbsent(stepDefinition, sd -> new ArrayList<>())); + + List stepDefinitionUsages = testStepsFinishedByStepDefinition.entrySet() + .stream() + // Filter out steps with without a step definition or with an + // ambiguous step definition. These can't be represented. + .filter(entry -> entry.getKey().isPresent()) + .map(entry -> createStepDefinitionUsage(entry.getKey().get(), entry.getValue())) + .collect(toList()); + return new UsageReport(stepDefinitionUsages); + } + + private UsageReport.StepDefinitionUsage createStepDefinitionUsage(StepDefinition stepDefinition, List stepUsages) { + return new UsageReport.StepDefinitionUsage( + formatSource(stepDefinition), + formatSourceReference(stepDefinition), + createStatistics(stepUsages), + stepUsages + ); + } + + private String formatSource(StepDefinition stepDefinition) { + return stepDefinition.getPattern().getSource(); + } + + private String formatSourceReference(StepDefinition stepDefinition) { + return sourceReferenceFormatter.format(stepDefinition.getSourceReference()).orElse(""); + } + + private UsageReport.Statistics createStatistics(List stepUsages) { + List durations = stepUsages.stream() + .map(UsageReport.StepUsage::getDuration) + .collect(toList()); + return Durations.createStatistics(durations); + } + + private Function createStepUsage() { + return testStepFinished -> query + .findTestStepBy(testStepFinished) + .flatMap(query::findPickleStepBy) + .map(pickleStep -> createStepUsage(testStepFinished, pickleStep)) + .orElseGet(() -> new UsageReport.StepUsage("", Duration.ZERO, "")); + } + + private UsageReport.StepUsage createStepUsage(TestStepFinished testStepFinished, PickleStep pickleStep) { + String text = pickleStep.getText(); + String location = findLocationOf(testStepFinished); + Duration duration = Convertor.toDuration(testStepFinished.getTestStepResult().getDuration()); + return new UsageReport.StepUsage(text, duration, location); + } + + private String findLocationOf(TestStepFinished testStepFinished) { + return query.findPickleBy(testStepFinished) + .map(pickle -> uriFormatter.apply(pickle.getUri()) + query.findLocationOf(pickle) + .map(Location::getLine) + .map(line -> ":" + line) + .orElse("")) + .orElse(""); + } + + private Function> findUnambiguousStepDefinitionBy() { + return testStepFinished -> query.findTestStepBy(testStepFinished) + .flatMap(query::findUnambiguousStepDefinitionBy); + } + +} diff --git a/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java b/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java new file mode 100644 index 0000000..5419e9b --- /dev/null +++ b/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java @@ -0,0 +1,57 @@ +package io.cucumber.usageformatter; + +import io.cucumber.usageformatter.UsageReport.Statistics; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; + +import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; +import static org.assertj.core.api.Assertions.assertThat; + +public class DurationsTest { + + @Test + void testToBigDecimalSeconds(){ + assertThat(toBigDecimalSeconds(Duration.ofMillis(0))).isEqualTo(BigDecimal.valueOf(0, 9)); + assertThat(toBigDecimalSeconds(Duration.ofMillis(100))).isEqualTo(BigDecimal.valueOf(100_000_000, 9)); + assertThat(toBigDecimalSeconds(Duration.ofMillis(1000))).isEqualTo(BigDecimal.valueOf(1_000_000_000, 9)); + } + + @Test + void createStatistics_without_values(){ + Statistics statistics = Durations.createStatistics(Collections.emptyList()); + assertThat(statistics).isNull(); + } + + @Test + void createStatistics_with_even_number_of_values(){ + Statistics statistics = Durations.createStatistics(Arrays.asList( + Duration.ofSeconds(1), + Duration.ofSeconds(1), + Duration.ofSeconds(2), + Duration.ofSeconds(4) + )); + + assertThat(statistics).isNotNull(); + assertThat(statistics.getSum()).isEqualTo(Duration.ofSeconds(8)); + assertThat(statistics.getMean()).isEqualTo(Duration.ofSeconds(2)); + assertThat(statistics.getMoe95()).isEqualTo(Duration.parse("PT1.224744871S")); + } + + @Test + void createStatistics_with_odd_number_of_values(){ + Statistics statistics = Durations.createStatistics(Arrays.asList( + Duration.ofSeconds(1), + Duration.ofSeconds(2), + Duration.ofSeconds(4) + )); + + assertThat(statistics).isNotNull(); + assertThat(statistics.getSum()).isEqualTo(Duration.ofSeconds(7)); + assertThat(statistics.getMean()).isEqualTo(Duration.parse("PT2.333333333S")); + assertThat(statistics.getMoe95()).isEqualTo(Duration.parse("PT1.440164599S")); + } +} diff --git a/java/src/test/java/io/cucumber/usageformatter/Jackson.java b/java/src/test/java/io/cucumber/usageformatter/Jackson.java new file mode 100644 index 0000000..1aa12d1 --- /dev/null +++ b/java/src/test/java/io/cucumber/usageformatter/Jackson.java @@ -0,0 +1,43 @@ +package io.cucumber.usageformatter; + +import com.fasterxml.jackson.annotation.JsonCreator.Mode; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.core.util.Separators; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT; +import static com.fasterxml.jackson.annotation.JsonInclude.Value.construct; +import static com.fasterxml.jackson.core.util.DefaultIndenter.SYSTEM_LINEFEED_INSTANCE; +import static com.fasterxml.jackson.core.util.Separators.Spacing.AFTER; + +final class Jackson { + public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(new ParameterNamesModule(Mode.PROPERTIES)) + .defaultPropertyInclusion(construct(NON_ABSENT, NON_ABSENT)) + .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) + .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + .enable(DeserializationFeature.USE_LONG_FOR_INTS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET) + .build(); + static final DefaultPrettyPrinter PRETTY_PRINTER = new DefaultPrettyPrinter( + Separators.createDefaultInstance() + .withObjectFieldValueSpacing(AFTER) + ) + .withArrayIndenter(SYSTEM_LINEFEED_INSTANCE); + + private Jackson() { + } +} + diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java new file mode 100644 index 0000000..d23d6c5 --- /dev/null +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java @@ -0,0 +1,117 @@ +package io.cucumber.usageformatter; + +import io.cucumber.messages.NdjsonToMessageIterable; +import io.cucumber.messages.types.Envelope; +import io.cucumber.usageformatter.MessagesToUsageWriter.Builder; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static io.cucumber.usageformatter.Jackson.OBJECT_MAPPER; +import static io.cucumber.usageformatter.Jackson.PRETTY_PRINTER; +import static io.cucumber.usageformatter.MessagesToUsageWriter.builder; +import static java.nio.file.Files.readAllBytes; +import static org.assertj.core.api.Assertions.assertThat; + +class MessagesToUsageWriterAcceptanceTest { + private static final NdjsonToMessageIterable.Deserializer deserializer = (json) -> OBJECT_MAPPER.readValue(json, Envelope.class); + private static final MessagesToUsageWriter.Serializer jsonSerializer = OBJECT_MAPPER.writer(PRETTY_PRINTER)::writeValue; + private static final MessagesToUsageWriter.Serializer plainTextSerializer = new PlainTextSerializer(); + + static List acceptance() throws IOException { + Map formats = new LinkedHashMap<>(); + formats.put("json", builder(jsonSerializer)); + formats.put("plain.txt", builder(plainTextSerializer)); + ; + + List sources = getSources(); + + List testCases = new ArrayList<>(); + sources.forEach(path -> + formats.forEach((formatName, format) -> + testCases.add(new TestCase(path, formatName, format)))); + + return testCases; + } + + private static List getSources() { + return Arrays.asList( + Paths.get("../testdata/src/minimal.ndjson"), + Paths.get("../testdata/src/unused-steps.ndjson"), + Paths.get("../testdata/src/multiple-features.ndjson") + ); + + } + + private static T writePrettyReport(TestCase testCase, T out, Builder builder) throws IOException { + try (InputStream in = Files.newInputStream(testCase.source)) { + try (NdjsonToMessageIterable envelopes = new NdjsonToMessageIterable(in, deserializer)) { + try (MessagesToUsageWriter writer = builder.build(out)) { + for (Envelope envelope : envelopes) { + writer.write(envelope); + } + } + } + } + return out; + } + + @ParameterizedTest + @MethodSource("acceptance") + void test(TestCase testCase) throws IOException { + ByteArrayOutputStream bytes = writePrettyReport(testCase, new ByteArrayOutputStream(), testCase.builder); + assertThat(bytes.toString()).isEqualToIgnoringNewLines(new String(readAllBytes(testCase.expected))); + } + + @ParameterizedTest + @MethodSource("acceptance") + @Disabled + void updateExpectedFiles(TestCase testCase) throws IOException { + try (OutputStream out = Files.newOutputStream(testCase.expected)) { + writePrettyReport(testCase, out, testCase.builder); + if (!testCase.format.equals("json")) { + // Render output in console, easier to inspect results + Files.copy(testCase.expected, System.out); + } + } + } + + static class TestCase { + private final Path source; + private final String format; + private final Builder builder; + private final Path expected; + + private final String name; + + TestCase(Path source, String format, Builder builder) { + this.source = source; + this.format = format; + this.builder = builder; + String fileName = source.getFileName().toString(); + this.name = fileName.substring(0, fileName.lastIndexOf(".ndjson")); + this.expected = source.getParent().resolve(name + "." + format); + } + + @Override + public String toString() { + return name + " -> " + format; + } + + } + +} + diff --git a/testdata/.gitignore b/testdata/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/testdata/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/testdata/README.md b/testdata/README.md new file mode 100644 index 0000000..2feec25 --- /dev/null +++ b/testdata/README.md @@ -0,0 +1,16 @@ +# Acceptance test data + +The pretty formatter uses the examples from the [cucumber compatibility kit](https://github.com/cucumber/compatibility-kit) +for acceptance testing. These examples consist of `.ndjson` files created by +the [`fake-cucumber` reference implementation](https://github.com/cucumber/fake-cucumber). + +* The `.njdon` files are copied in by running `npm install`. +* The expected `.log` files are created by running a test that updates them. + +We ensure the `.ndjson` files stay up to date by running `npm install` in CI +and verifying nothing changed. + +Should there be changes, these tests can be used to update the expected data: + * Java: `MessagesToPrettyWriterAcceptanceTest#updateExpectedFiles` + * Java: `MessagesToSummaryWriterAcceptanceTest#updateExpectedFiles` + * Java: `MessagesToProgressWriterAcceptanceTest#updateExpectedFiles` diff --git a/testdata/package-lock.json b/testdata/package-lock.json new file mode 100644 index 0000000..92a2ec4 --- /dev/null +++ b/testdata/package-lock.json @@ -0,0 +1,965 @@ +{ + "name": "demo-formatter-testdata", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "demo-formatter-testdata", + "version": "1.0.0", + "hasInstallScript": true, + "license": "ISC", + "devDependencies": { + "@cucumber/compatibility-kit": "^23.0.0", + "shx": "^0.4.0" + } + }, + "node_modules/@cucumber/compatibility-kit": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-23.0.0.tgz", + "integrity": "sha512-+XapEOpxPm2c5iOD+eiWgRQLAN6MMal22dOeicm6Z4Vewp1XpEoPaS/G5l0ODMcIxhKi9GrUlyCsQ1Hd84M45w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shelljs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "fast-glob": "^3.3.2", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/shx": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.8", + "shelljs": "^0.9.2" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + }, + "dependencies": { + "@cucumber/compatibility-kit": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-23.0.0.tgz", + "integrity": "sha512-+XapEOpxPm2c5iOD+eiWgRQLAN6MMal22dOeicm6Z4Vewp1XpEoPaS/G5l0ODMcIxhKi9GrUlyCsQ1Hd84M45w==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + } + }, + "fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "shelljs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "fast-glob": "^3.3.2", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "shx": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", + "dev": true, + "requires": { + "minimist": "^1.2.8", + "shelljs": "^0.9.2" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} diff --git a/testdata/package.json b/testdata/package.json new file mode 100644 index 0000000..46a8787 --- /dev/null +++ b/testdata/package.json @@ -0,0 +1,15 @@ +{ + "name": "demo-formatter-testdata", + "version": "1.0.0", + "description": "The demo formatter uses some test data for acceptance testing.", + "main": "index.js", + "scripts": { + "postinstall": "shx cp node_modules/@cucumber/compatibility-kit/features/**/*.ndjson ./src" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@cucumber/compatibility-kit": "^23.0.0", + "shx": "^0.4.0" + } +} diff --git a/testdata/src/ambiguous.ndjson b/testdata/src/ambiguous.ndjson new file mode 100644 index 0000000..4e6b887 --- /dev/null +++ b/testdata/src/ambiguous.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Ambiguous steps\n Multiple step definitions that match a pickle step result in an AMBIGUOUS status, since Cucumnber cannot determine\n which one to execute.\n\n Scenario: Multiple step definitions for a step\n Given a step with multiple definitions\n","uri":"samples/ambiguous/ambiguous.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Ambiguous steps","description":" Multiple step definitions that match a pickle step result in an AMBIGUOUS status, since Cucumnber cannot determine\n which one to execute.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"Multiple step definitions for a step","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step with multiple definitions"}],"examples":[]}}]},"comments":[],"uri":"samples/ambiguous/ambiguous.feature"}} +{"pickle":{"id":"3","uri":"samples/ambiguous/ambiguous.feature","astNodeIds":["1"],"tags":[],"name":"Multiple step definitions for a step","language":"en","steps":[{"id":"2","text":"a step with multiple definitions","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"REGULAR_EXPRESSION","source":"^a (.*?) with (.*?)$"},"sourceReference":{"uri":"samples/ambiguous/ambiguous.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"REGULAR_EXPRESSION","source":"^a step with (.*)$"},"sourceReference":{"uri":"samples/ambiguous/ambiguous.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["4","5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"step","children":[]}},{"group":{"start":12,"value":"multiple definitions","children":[]}}]},{"stepMatchArguments":[{"group":{"start":12,"value":"multiple definitions","children":[]},"parameterTypeName":""}]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"AMBIGUOUS","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/testdata/src/attachments.ndjson b/testdata/src/attachments.ndjson new file mode 100644 index 0000000..8949657 --- /dev/null +++ b/testdata/src/attachments.ndjson @@ -0,0 +1,61 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Attachments\n It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type.\n\n Scenario: Strings can be attached with a media type\n Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.\n\n When the string \"hello\" is attached as \"application/octet-stream\"\n\n Scenario: Log text\n When the string \"hello\" is logged\n\n Scenario: Log ANSI coloured text\n When text with ANSI escapes is logged\n\n Scenario: Log JSON\n When the following string is attached as \"application/json\":\n ```\n {\"message\": \"The big question\", \"foo\": \"bar\"}\n ```\n\n Scenario: Byte arrays are base64-encoded regardless of media type\n When an array with 10 bytes is attached as \"text/plain\"\n\n Scenario: Attaching PDFs with a different filename\n When a PDF document is attached and renamed\n\n Scenario: Attaching URIs\n When a link to \"https://cucumber.io\" is attached\n","uri":"samples/attachments/attachments.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Attachments","description":" It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Strings can be attached with a media type","description":" Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.","steps":[{"id":"0","location":{"line":15,"column":5},"keyword":"When ","keywordType":"Action","text":"the string \"hello\" is attached as \"application/octet-stream\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":17,"column":3},"keyword":"Scenario","name":"Log text","description":"","steps":[{"id":"2","location":{"line":18,"column":5},"keyword":"When ","keywordType":"Action","text":"the string \"hello\" is logged"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":20,"column":3},"keyword":"Scenario","name":"Log ANSI coloured text","description":"","steps":[{"id":"4","location":{"line":21,"column":5},"keyword":"When ","keywordType":"Action","text":"text with ANSI escapes is logged"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":23,"column":3},"keyword":"Scenario","name":"Log JSON","description":"","steps":[{"id":"6","location":{"line":24,"column":6},"keyword":"When ","keywordType":"Action","text":"the following string is attached as \"application/json\":","docString":{"location":{"line":25,"column":8},"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}","delimiter":"```"}}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":29,"column":3},"keyword":"Scenario","name":"Byte arrays are base64-encoded regardless of media type","description":"","steps":[{"id":"8","location":{"line":30,"column":5},"keyword":"When ","keywordType":"Action","text":"an array with 10 bytes is attached as \"text/plain\""}],"examples":[]}},{"scenario":{"id":"11","tags":[],"location":{"line":32,"column":3},"keyword":"Scenario","name":"Attaching PDFs with a different filename","description":"","steps":[{"id":"10","location":{"line":33,"column":5},"keyword":"When ","keywordType":"Action","text":"a PDF document is attached and renamed"}],"examples":[]}},{"scenario":{"id":"13","tags":[],"location":{"line":35,"column":3},"keyword":"Scenario","name":"Attaching URIs","description":"","steps":[{"id":"12","location":{"line":36,"column":5},"keyword":"When ","keywordType":"Action","text":"a link to \"https://cucumber.io\" is attached"}],"examples":[]}}]},"comments":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"id":"15","uri":"samples/attachments/attachments.feature","astNodeIds":["1"],"tags":[],"name":"Strings can be attached with a media type","language":"en","steps":[{"id":"14","text":"the string \"hello\" is attached as \"application/octet-stream\"","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"17","uri":"samples/attachments/attachments.feature","astNodeIds":["3"],"tags":[],"name":"Log text","language":"en","steps":[{"id":"16","text":"the string \"hello\" is logged","type":"Action","astNodeIds":["2"]}]}} +{"pickle":{"id":"19","uri":"samples/attachments/attachments.feature","astNodeIds":["5"],"tags":[],"name":"Log ANSI coloured text","language":"en","steps":[{"id":"18","text":"text with ANSI escapes is logged","type":"Action","astNodeIds":["4"]}]}} +{"pickle":{"id":"21","uri":"samples/attachments/attachments.feature","astNodeIds":["7"],"tags":[],"name":"Log JSON","language":"en","steps":[{"id":"20","text":"the following string is attached as \"application/json\":","type":"Action","argument":{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}"}},"astNodeIds":["6"]}]}} +{"pickle":{"id":"23","uri":"samples/attachments/attachments.feature","astNodeIds":["9"],"tags":[],"name":"Byte arrays are base64-encoded regardless of media type","language":"en","steps":[{"id":"22","text":"an array with 10 bytes is attached as \"text/plain\"","type":"Action","astNodeIds":["8"]}]}} +{"pickle":{"id":"25","uri":"samples/attachments/attachments.feature","astNodeIds":["11"],"tags":[],"name":"Attaching PDFs with a different filename","language":"en","steps":[{"id":"24","text":"a PDF document is attached and renamed","type":"Action","astNodeIds":["10"]}]}} +{"pickle":{"id":"27","uri":"samples/attachments/attachments.feature","astNodeIds":["13"],"tags":[],"name":"Attaching URIs","language":"en","steps":[{"id":"26","text":"a link to \"https://cucumber.io\" is attached","type":"Action","astNodeIds":["12"]}]}} +{"stepDefinition":{"id":"28","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the string {string} is attached as {string}"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"29","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the string {string} is logged"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"30","pattern":{"type":"CUCUMBER_EXPRESSION","source":"text with ANSI escapes is logged"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"31","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the following string is attached as {string}:"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":18}}}} +{"stepDefinition":{"id":"32","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an array with {int} bytes is attached as {string}"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":22}}}} +{"stepDefinition":{"id":"33","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a PDF document is attached and renamed"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":31}}}} +{"stepDefinition":{"id":"34","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a link to {string} is attached"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":38}}}} +{"testRunStarted":{"id":"35","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"36","pickleId":"15","testSteps":[{"id":"37","pickleStepId":"14","stepDefinitionIds":["28"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":11,"value":"\"hello\"","children":[{"start":12,"value":"hello","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"},{"group":{"start":34,"value":"\"application/octet-stream\"","children":[{"start":35,"value":"application/octet-stream","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"38","pickleId":"17","testSteps":[{"id":"39","pickleStepId":"16","stepDefinitionIds":["29"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":11,"value":"\"hello\"","children":[{"start":12,"value":"hello","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"40","pickleId":"19","testSteps":[{"id":"41","pickleStepId":"18","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"42","pickleId":"21","testSteps":[{"id":"43","pickleStepId":"20","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":36,"value":"\"application/json\"","children":[{"start":37,"value":"application/json","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"44","pickleId":"23","testSteps":[{"id":"45","pickleStepId":"22","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"10","children":[]},"parameterTypeName":"int"},{"group":{"start":38,"value":"\"text/plain\"","children":[{"start":39,"value":"text/plain","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"46","pickleId":"25","testSteps":[{"id":"47","pickleStepId":"24","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"48","pickleId":"27","testSteps":[{"id":"49","pickleStepId":"26","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"\"https://cucumber.io\"","children":[{"start":11,"value":"https://cucumber.io","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCaseStarted":{"id":"50","testCaseId":"36","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"37","timestamp":{"seconds":0,"nanos":2000000}}} +{"attachment":{"testCaseStartedId":"50","testStepId":"37","body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream","timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":4000000}}} +{"testCaseFinished":{"testCaseStartedId":"50","timestamp":{"seconds":0,"nanos":5000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"51","testCaseId":"38","timestamp":{"seconds":0,"nanos":6000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"39","timestamp":{"seconds":0,"nanos":7000000}}} +{"attachment":{"testCaseStartedId":"51","testStepId":"39","body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"51","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"52","testCaseId":"40","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"52","testStepId":"41","timestamp":{"seconds":0,"nanos":12000000}}} +{"attachment":{"testCaseStartedId":"52","testStepId":"41","body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepFinished":{"testCaseStartedId":"52","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":14000000}}} +{"testCaseFinished":{"testCaseStartedId":"52","timestamp":{"seconds":0,"nanos":15000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"53","testCaseId":"42","timestamp":{"seconds":0,"nanos":16000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"53","testStepId":"43","timestamp":{"seconds":0,"nanos":17000000}}} +{"attachment":{"testCaseStartedId":"53","testStepId":"43","body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"53","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"53","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"54","testCaseId":"44","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"45","timestamp":{"seconds":0,"nanos":22000000}}} +{"attachment":{"testCaseStartedId":"54","testStepId":"45","body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain","timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":24000000}}} +{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"seconds":0,"nanos":25000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"55","testCaseId":"46","timestamp":{"seconds":0,"nanos":26000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"47","timestamp":{"seconds":0,"nanos":27000000}}} +{"attachment":{"testCaseStartedId":"55","testStepId":"47","body":"JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoVW50aXRsZWQgZG9jdW1lbnQpCi9Qcm9kdWNlciAoU2tpYS9QREYgbTExNiBHb29nbGUgRG9jcyBSZW5kZXJlcik+PgplbmRvYmoKMyAwIG9iago8PC9jYSAxCi9CTSAvTm9ybWFsPj4KZW5kb2JqCjUgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDE2Nz4+IHN0cmVhbQp4nF2P0QrCMAxF3/MV+YF1TdM2LYgPgu5Z6R+oGwg+bP4/mK64gU1Jw73cQ0potTrSlrzD+xtmMBJW9feqSFjrNmAblgn6gXH6QPUleyRyjMsTRrj+EcTVqwy7Sspow844FegvivAm1iNYRqB9L+MlJxLOWCqkIzZOhD0nLA88WMtyxPICMexijoE10wyfViMZCkRW0maEuCUSubDrjXQu+osv96M5GgplbmRzdHJlYW0KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8L1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9FeHRHU3RhdGUgPDwvRzMgMyAwIFI+PgovRm9udCA8PC9GNCA0IDAgUj4+Pj4KL01lZGlhQm94IFswIDAgNTk2IDg0Ml0KL0NvbnRlbnRzIDUgMCBSCi9TdHJ1Y3RQYXJlbnRzIDAKL1BhcmVudCA2IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFsyIDAgUl0+PgplbmRvYmoKNyAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUj4+CmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aDEgMTY5OTYKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA4MDA5Pj4gc3RyZWFtCnic7XoJeFRF9u+pureXrN0J2TrppG+nkw6kA4EECEtMOhugkT1gwiSSAJGAIEtAQVGaGVCJKI4LDuiI+6CO0lnADi4wMjojLjDquAsIjOLMIOgoruS+X1V3gIj65sv7z3uf75u+Ob86derUqapTp869N93EiKgPQKWBo8srRtFH9C4R80Pad/SE8ZN9g357HRE/gvrq0ZOnlIY/Y1qH9rdQHzh+cm7esjHbj6F9Ner1U8vHVk+4Ze4XaNpHFHPbzPkNCxlny9DuRXv5zMuXaPfa3/wHkXEXqOqShbPnv7S8ZhNRVBzql81uaF5ISRQG+4XQt86et/ySu6oLu4jsOUTmQ02z5i97puTkEkwY45m3NDU2zDoY9zzscTP0hzZBEJsf5kR/zJEymuYvWRa/nu0nMtRDVj9vwcyGRE885qc0ob1tfsOyhYb2KB/aLkRdu6xhfmNi/aD34Qw7ZOULFzQv0bNpA/h5on3h4saFmW+M3UmUaSWKeAYyhczEKYaYroMXvqymz6iQfksmyK2US1Nh7ffQNaCukPzoWcLmD3zQ31TUNY7KrPTN1m+utEpJj0+1lESGahy7FuxXgIvRGFwMI14EFHrhNACXoWFxwwzSZi5fPI+02YsbLyWtqXHGYtLmNSy5jLQzY5PBtmmRI6Z9uqXwC3OKWYrvO5yVLcoXJ4zc/s3WU7OtZBajh501My79QBQX8kCciCWUZukboipqpCXwT5Br1nX9sLjOsqAo17Ob4SGzYZMhH1NJCZbKX+gSHms28AijysVHpe95ZOz4cePJC7tLDK91TWT5piLW5hWbgdFUt+FJsWuYTdAXpVRLivRCTtALcv1xQR+iB+v2p+TZWTymcmnjYuiejaG5CD2OlTJJkRScY6y0UICWMXoqTQURxf9fvTb87y52549fylPqIulgE00Tu6riTNJc8oV4Bm9eHuI5RVNTiFewF31DvHqWjoGSoRXkjeCISmgxzaEGmkdjsXtTEReLqRmSBSQicgiidhBiqAGtQrKAltByWggtjc6n+ZDPhu5lQI36g85Y02gStGbTUvANkPasndF7GJp5GGEQLg0zaJK2zx2tDLXF4AU2QB6c4QA55rzQeHMwQhPamkOjN8vVXA6cRQOM5xzh/38+6mF5zv/PbDRTZa/6ERXz4ZRh2EE2ULLhd2RT3bh7kP4R6Kgou+boR0W7KPnf0SkQIqIt9BibQ4/RTnqWnUCvrdRJHfRnSqRyuotW0G10HSJ1GiRrsaeTEMHldBuz6R3I6Pciku+ll6F7EV1DOyiBJekf00pao7yGXmsoitIRHRMQKTeyC/WlyDoH1F8hF1yIyFnIfHq1fpN+i/4APUidyp/1UxSB0zET18v6J4a39PcQ0bV0O22kA+yWsG04URfh3HUqv0VMbVLqVKbP1r/BDJx0BeagImZfZru4B9Yb6SOWxFYoZbByv+7X/wgtO9UhNjfRDjaEjeZOQ60+Vn+ZEjDGMljdSG20HVeAnqZ3WKThhP6AfoJslINTthL+eIXtUrpOreoqhscM8FI/Go6WBfQM/Yn2MRf7A19giDTkGbyGK/XXkREH0RTM9nfo+SH7kl+Da6XyvDpKL8WZX0O/Ft6m5+gDlsxy2Xg2lffjC/jdymJkzhx5EmfhLK2l38D6fuZh23kk36vcrz6qfmtM7TqoR2NH3HQn7q1/YFFYqcaa2S/ZG+wwL+PT+Z38kHKb+rD6qqkBq74YWeJGepS+ZLFsGJvIfsGa2Ap2Hfs128heZvvYUV7Cq/il/LjSpCxSnlZLcU1Wm9VfGa413GA82lXd9ceuv3R9qefp19JExMMqzP52uhsr66S99DauA3SIGVgEi8alMSebwq7CdQ27kd3HtrCHWQdG2ccOsY/ZZ+wL9i1HouRGnsKdPB2Xiy/mV/Db+F18L659/J/8ayVRSVc8yhClUKlRFmBW1yk349qmfKAmq3tVHX7OM2wwbDZsMTxqeNZwwhhp+iVusS99d/+p7FP7u6jr+q4NXW1dHfoHyP42xJSdHHgSmYi81YDcvQw5/0HE+WssEr5LZtmsiF0Iz0xnc9kitgyeXM02sQfl3B9nT8FLb7LjmHMUt8s5D+BDeCkfj+ti3sgX8Zv5LbyDv8G/UUxKhGJR4pVsZbRSpzQqS5TlygbFr7ykvK8cUk4q3+HS1XDVoaarbtWjjlanq0vVu9WP1I8MtYYXDX8zhhvnG681BoyfmoaaikwTTBNNdab1pu2m1831iM7dtI2eOPvss4PKKqVC2UY38XzVxl/hryCep9MsZSxHpPIt7Hp+NevgGYZlxpF8JBtHJ1Q3fP0838xP8pHKWFbJJtNcPihozRinPoKiUN1Nx9SnsLZXYHmZMZJdw48bI6kNjwXDMeZzykDVo7xI7ygHmEm9l95Vw1kiO8Z/p0xAFDytFhmqyancRY8ri9jVtI1X4JHjW/M6xPE49gjyQhXLY18peErk4xBFBcph+hVdyt+iYzjH19MdbJY6m26ifLYCT+AP4VT0M1xmzDbGsxf4HLWF92EdxNWHsbrhLIMphjhazeqUTcbj/G3c3faq4bRf+T1mv5c/roxVTxgmsSacgKvpWlqkr6Llhmr1VTabFDaVMtWDyG4rlDzViXIlskotctp2nO4dyAMlylhIkhA5FyIupiBDbML1G+QJFRE0B2f8ImSxV6jDWMUDNNsQzZB1kI1f7JpE0/SHaKM+my7Tb6H+yAfX6StgcQv9jdbTFram6yrcR9NwcvazCw2j+F7DKL0/b+Fv88l8Q8/9hbczWRL9HdfjqBThOa5FfZMmU7G+Tv8rorsvMuxGmkEX0BGs8hOMMEbZRfld43irPkpZiPUeoIn673QHC6cmfR6Np6foQZOBGkwe7LGfvYr1XkWNfJK+RGnsmgM/rIcXvPDWUuSftd6yKVUl3uKi8wpHjhg+rGDI4Py8QQNzB/TP8WT365vlzsxwpTs1R1qqPSXZlpSYEB/XJzbGaomOiowIDzObjAZV4YxyKlyj6jW/u96vul1jxvQXdVcDBA1nCer9GkSjeur4tXqppvXU9ELzku9peoOa3tOazKoVUmH/HK3CpflfLndpATZtYjX4G8tdNZr/mOTHSv5myUeBdzrRQatIairX/Kxeq/CPuryppaK+HOZaI8LLXGWN4f1zqDU8AmwEOH+ia2ErSyxikuGJFSNa8QQchUn5k13lFX6bq1zMwK9kVjTM8k+YWF1RnuJ01vTP8bOyma4ZfnKV+i0eqUJlchi/scxvksNoc8Rq6AatNWdXy7qAlWbUeyJnuWY11Fb7lYYaMUaMB+OW+xOvPJJ0pgrjsWXV153dmqK0VCTN0US1peU6zX/PxOqzW50Ca2pgA3155qj6llEYeh2cWDlZw2h8TU21n63BkJpYiVhVcH2NrgohqZ+r+cNcpa6mlrn12JrkFj9NWu5sS072duoHKblCa6mqdjn9xSmumoZye2sctUxa3m7zaraeLf1zWq0xQce2RltCTGTU2Uzj6TbJSXXBVU467VkmZuQ6HwHh12ZqmEm1C2saJqBxGLXMHAY1fGoYevlnYUfm+MPK6lusI4Rc9PcbMq0ureULQgS4jv2zp6QhJDFmWr8gwYo4OR1qaO/m/R6PPztbhIipDHuKORbJ+pD+OZcHuMu10KqhgPtoAnzbUDMiF+53OsUG3xDw0gxU/L6J1cG6RjNS2sib66nx83rRsqu7JX6KaPF1t5zuXu9CJHfIJ+54v9l9+s9iTehT0TTCzxJ+orkx2F452VU5cVq1VtFSH/JtZVWPWrB92Om2EOfvU1atpPAQx1MU2YqgrD2tLCrVkX41E39GGdSzAiYzolJKmDbKb60fE8SacKfz3+wU0E+IXrI40y00Tf8IT8/6yB71HtOLbFEwYdwqK6umtbSE92hDqAUHPD9UIOKpqtqplflpCk5mJv4C+q5hgmpS/F64rEwoIP6ColC1h2JKiK/BR0Rn/5xRSHQtLaNc2qiW+paGgO6b4dKsrpZO/ix/tmVhRX134AT0HTek+Eetq4GvmtgIHApOpa0udv3EVi+7fvK06k4r3vyvr6pu44yX1ZfWtGagrbpTI/JKKRdSIRQVTVSokmGRbdws9VM6vUQ+2apKgazPDDCSMnO3jNHMAA/KrN0yDpkalHmlTHxEjimrqj47euSRrOkvb3h4b6HaCLO5N69CeIT5aYFRIYoMC+udbdNPC0ywHRUe/p+xjZc8S0RE72yfs9yevjXDtjUy8vtKvbTdUyBsx0RF/cds94mO7p3tc5bb07fhBiRGq/V/yHZPQQRCMik2tne2z1luT99GImxS4uJ6Z/uc5Vp6Do2wSU1I6J3tPj89mAW2taSk/yHbMT1HQtg4bbbe2Y7/adsxsJ1pt/fOduL3BT33LRapJFvTemc7+acHi0NIDnC5emf7nOX2HCwRIZnndvfOtuOnB7Mh/of269c7287vC9J61FIQ7iNycnpnO+P7Aq1HLRXhXpaX1zvb5yw3s0ctHfFfOWxY72z3/74gu0fNjfifXFTUO9uDvy8Y0HMkhGRtRUXvbA//viC/50gIyVmVvfp3Kt6yvy/o6ds8EZJcfkmEixRxq3bGOGMyAeIrkO80Zdd3XgN9S5q6S3wDMpBI3WHYAb39XpuRR0aWTjFJNJoiIsBLZAH96w7BEBhvjOCMhsgoNEtE87cdgkHzt94YwRl4Gl6vSb5mhwV4c7umMjXA2BNGjfFchSngtzGmYQYB/ag3wmrlU8hssXBh47OOyEjJHOqIipLMd5AYBdMFiWBg0bx9Y5LHetIjP3WF1s9Bp47UfWgttBZScXHhqcJBA5nn9AcOGOKMd8bwPl2paktXiiHqsce++ReeAiv1o2qaWoRsmsru9iY6yB7Ppyh1hrqwKRGNyqWGBWGNEeb4gH5EDh0DxjtJcKl2gVmxbxu+iTuZrA6KHWEbZC+JHZtcYp8YW2ubZG+InZ/cYF9mXBZ/kp9MslICs0QlJk5IqE9YmKAk2C03W++xcqtVTbGHm2gHf4SYvqtDOAL+3OWNtlqNU6yMsdv72NWIRLw3dIhtSRTuERsA5qvtUXB1ojcqoL8nPQXmEzlLMH+XLosSpsKysgf7o1hUsgO19kz3YFE+keYaPNDBHAnwrrdWGErIt5rFENZoYd9qFjJrhsmbkT3YYSo2jTcppkgZH5GixaRFRPAppiSxVSa7GN2EfkbwYlxTgpiGyZY2uCDJM876efcu1HnGnkJxBLJFHs/JRUI29hiAio+dqkND8bHY4bl1hacWFbKY2OHDY4djE+sILR62aDFLNBpd6RRjpfw8iokzORMS8vOGMqc7y+1KNyoX78j5pPPjruMs7r2/smj23dHwtjUz1516h0+MHDZ17YqH2dTE+zuYgykskvXt2t/1tVXbuqOJ3X5tWdND4iwU60eVVkTCQKXV2ydReiFJok1i34D+udyDrG7G3c1kdjMZ3Yyrm0nvZpzdjAbGu1Jwanpc+oiwC8LKM6amN6avCLspbHXGQ30ezXlWiQpLTE5KHFiZ80aiIYVP4dyax8KTas21YbXhtRG1kbVRc81zw+aGz42YGzk3qsPdkWXJcmdkZfQbmjEtvCZilntW3yWuJRm+jFvD74q8pe8dObcPfCD84cj7sx7o2+5+zp0g1yK2KL2bcXUzGd1MaL3G7iUYuxdl7F4mDkFA3++NTRs+zZyVGRmuJmvueDViQGpygD/iTbfliBBx2Ipt423TbVtte21Gi81hW2A7YFMdtvU2bnsapxtZPBj73jihbmVexq1sH+PErIyLs9AelzBYnglrdMxgxgbUps5L5an2eJMqpiE6gfmwQxwYwXj7WCzg7AMiHMksOcPm7ZM0OE90HyLyiy0piCJibQkiem2a6GnTRC+bVazKJqNXtGLvd/BfkEn/bLtMhxnZMLTNPnxfNssWY4r+YI52CKOSEf2zxfETJsB8vl1YyU6WM3DiJNbn7crjxXm+PJ4njncGyamQVSY2Leh8LoNErkhGi0PMTZNRqGVYrGLJFjl3iyaULQH9G69bTMESLca3RApjFqMY2ZJ+gFgxjUemsw0Knca6RWO7T6Q4ex4rysXjrHWLPMF0ukicyc/P5M5ji3E8URYfW4TTiVO8aLHniPWULHBK8YfDmoijWrbc683qn+YyxOW4Y6yx1j5WxZgepaVQWF9TCjP0B6TFoeqMdqVQuisq0twvPIX1zQoLN3rUFHJYU1MYYT5I4UGQCTzbs2rVKjo9m7pFrG7xorozAqHUp0DmgiGDs9xZA/iQwUMLhg7Nz0tISDS5RW6Ij0tMwJXG4+NECnEXt1nWXrVi2ZDMW5/fOL5kWPavJ1/99LQYf2TznBVzExJyU1bvvGPqnOev3vs2O89+6eLG8vNcSZl5568aN3p5X4dnzFWzkybVTipw2VP7hGfkl6yonbb5ot+LDJKhf8azDRspkTk6KRJ3K7EDEYEQY+5mTN2MsZsJF2Hucg8OE1EyGYzPxohFRoUzhRKsYR5LuDHBrkRYrOmUzqJiZW6OlfEQGy76x2ZGMt1krgirqDctNPlMN+Ol3KSZ7jH5TbtM+0xGk7gziHuLScSViBSTuJFER0vmKxlykpHpHOEkYw/MCW+EiD2TUWZ1EeAyse/gcymJDW295MwtWO7M50esxwpFhi+0Hvkct+Fj4j4cgzQek59vfUHk8pBqZqLYBveQGNeQ/JiCmPx4V0yc2EFuTb6wcMa8nNWr27dt6+Ppm3bvZmtR43185jpmmtd147pTt47NwfNTJ1UpyGRJjn1PKf3oIIgr/do8qY5OJUtJbRvp8AYUV3tsfJ6lpL8injJyJWrABaCtoJ2K+M3JdCUNcitwJcgH2graCdoHwtswULRqoAWgzaCDokVJVextmsNakqXY0NeG82VREuk4SAcp5ADmgsaDpoPWgzaDjFJPSBaAVoJ2gk7IFq+S2HZLPuae2HaDLNrnzsuT1YZgtbZOVtsvqgmWYycGy/Lzg2ojgmqDBgfFA0qDZVZOsIzNzPOJMjwqb1cJHkKwyARMfCGQ8T+ShTG85NyjxJMfxBVjSOJVYtsz3HmbdyoqMYUrjGaRQ9+lsLaomLyScK7z4xRLDv4JPxZs4cfao2PyNpdcwA/RVtBOkMIP4fqAf0Ar+UHhc2AxaDNoJ2gv6DjIyA/iOoBrP99PFv4+5YKKQdNBm0E7QcdBJv4+0MrfE/8rlij4YhDn7wGt/F0s612ghb8D7h3+Dqb2WlvB8LxOyXhyQ4wjM8QkpoSY2IS8AH+17et+iCg3dhoR9aSSjsfvfCW9LXOQI6AktRXOcQT44XbN47inZCB/nfwgjpm8jpFfJw00AVQPWggygnsD3BvkA90MugfkByHKgFaQxveAXgK9QQNBXtAEkJnva8MwAb63zV3qKEngr/A/4a3ZwV/mf5blS/x5Wb7In5PlCyjTUO7hz7elOagkAu2EPlaUVpS5aDfwP7RnxDr0khi+E75zAHNBxaDxoOmg9SAj38nT22Y5YmHkSdpjxnswb6OPZfkQ3Wcm71yH112GANQEuEecBw6wWdvs5l73ho2oCnDfdAs4Ae7V68AJcF+5CpwA97zLwQlwz5oLToB72nRwAtzjq8ABAvzuJzKyHAXjL2VaiYVfAS9dAS9dAS9dQSq/Qlz0tSrmdmdbdjY8tsnr6Zft8O1gvqeYbxLz3cd8jcx3DfOtYr5C5ruY+TzMZ2e+NObzMt+TbBhc4WPejh7V4d4k5tvDfI8xXzPzuZkvk/kymE9jBd4Ad7adny+LClm0l4hDh/K8ImQfC3fCo07EvBM5YSdwL0iXNS+UtPSgsi1NlOnt2cXB+oAReQtKxvDd6Lgb27CbDoBUbNBuhNFuGNkNAxZgMWg6aBfoOEgHGaGdjomvl2gB5oKKQdNBK0HHQUY5neMgTgtCU9wqJ5YbmvR4UeO7cYkfQzi505tqtVs91jHKejuzpLHxaXoaLyD5f7fYGHNMgEVt/zLqqy+jKKwkjN/E11MqNuLmULm+7etUR4D9ps39pKMknt1BaSqijg0nN8tEOYyaZX0I2c2iHEx2/ijKvDb7VHSztLlzHDtYtOi13fG1/YjjY3uAgz1qf9LxphZQWZvjr5A8ut3xun2t44XcgBmSp9x40Wxz7NCkaqd9mOOxPVJ1FRo2tTmuEcV2x9X20Y5L7bKhMdhwcTNqXotjknuaYwzsldtnOLzNsLndUWy/2FEY1Boi+mx3DMQUPEE2G5PtZ5eDutKkwSkFAdbkzTFtMFXjHWqoKc+UY3KaHKZUU4opzhxrtpqjzZHmcLPZbDSrZm4mc1xAP+j1iOeJOKP8calRlT9glLyVk/wJpPxZI2dmTheQv49SySsnl7JK/66ZVDlD85+c7Aqw8InT/AZXKfPHVlJlVal/mKcyYNIn+Qs8lX7ThF9UtzJ2Uw2kfn59gFFVdYDpQrQmRXxH20mMxay5MUWUfdfcWFNDSQmXFycVxxbFDB9V/gNQH8Izj42epB58qn9D5eRq/yOpNf48weipNZX+W8WXuJ3sM3aioryTfSqKmupOpYh9VjFJyJWi8pqaygCbKvVIY59CDxHzqdQz48Ys9EgzpwX1NgX1MtEfehmigF5YGGVKvcywMKmnMqHX2pxRUd6akSF1EjVqljrNidrZOnsyoZOZKXUSfLRH6uxJ8Akdf5FUsduhkmaXKiyZ7FLFzpKlytQzKrkhlbWnVdbKkRR2Rsce1Ik62K0TdRA6nn/301iK5+H2kTUza8UX4PWuikZQvf+Gy5uS/L4ZmtY6syb0zbi7fsbMJlE2NPprXI3l/pmucq11ZO0PNNeK5pGu8laqraiqbq31Npa3jfSOrHA1lNe0j54wuKDHWGtPjzV4wg8YmyCMDRZjjS74geYC0TxajFUgxioQY432jpZjkYzxCdWtZiqtKasNlu08IhzxWp/irClNsC4sksE70pl0TcoOPK1soQhPjT/SVeqPAomm/iX9S0QTzpRoiha/cgg1JV0z0pmyg20JNVkhjnGVkmfJ0uallFQxpzz414wPREuWCocH0dP8Yx+0Vfi9DeXNS4gq/dmTK/3FE6dVt5pMkNaLJflHdMsiIirw+B8UDoBwhBAqymlFISsUsrCwkOK5+780VJaJU+DjT7YzbxpbQs01ij+tsoojFVSFvk7egWcpcXtorsECm5mHNXfbCE3b4wm9YpFYczctWRriQr5YEiqDPdGludslpz/CWZ7THlsCg+KjkMLEx6AoeM1nlGT4Z8Qu+sqsi1+k610URmH6KQqncPnbywhgJF6pTlEURQGjJVooGmglCzAG+B0eQ2OAfSgWGEd9gPHAbymB4oCJFA9MAn5DNkoEn0w28CmUDLRLTKUUYBrZ9a/x6CtQo1SgEw+2X1M6aUAX8CvKICcwk9KBbuCXlEUuYF+8B35J/cgNzJbooSz9JOVQX2B/iQMoG5hLHuBA6g8cBPyC8mgAMJ9ygYNpoP45DZE4lAYBCygfOIwG6/+i4RJH0BDgSImFNBR4HhUAi2gYsJiG65+Rl0YAS2gksJQKgWXAT6mczgNWUBFwFBXrJ2g0eYFjqAR4PpUCL5BYSWXAC6kcOJZG6cdpnMTxNBo4gcYAJ9L5+ic0SeJkugBYRZX6MZpCY4FTJV5E44DVNF7/J9XQBOA04DH6BU0EX0uTgXVUBbxY4nSaov+D6mkqsIEuAs4A/p1mUg1wFk0DNtIvgJdQrf4xzZbYRHXAOXSxfpTmUj34SyXOowbgfJoB+WU0E7hA4kKapX9Ei6gRuJhmA5slLqEm/UNaSnOAl9Nc4BXAv9EyuhS4nOYDr6TLgFdJXEELgFfTQuA1tEg/Qisl+qgZuIqWAH9JS3Xxm8LLgaslrqEr9EN0LS0DXkfLgdfTlcC1dJX+AbXQCuANdDUk64Af0I10DfAmWglcT6uANwMP0q/pl8Bb6FfAW2m1foBuk3g7rQFuoOuAd9D1aP0N8ABtpLXATdSi76c76QbgXbQO+FuJd9NNwM20HngP3Qy8F/g+3Ue/Bt5PtwAfoFuBD9Jt+nv0EN2uv0u/ow3ALXQH8GGJj9BvgI/SRuDv6U7gYxIfp7uAW+m3QD/dDWwFvkNttBnYTvcAO+g+/W3aRvfrb9F2iU/QA8AAPQjspIeAOyQ+SVuAT9HD+pv0ND0CfEbiTnoUuIt+D/wDPQZ8lh4H7qat+hv0R/IDn6NW/a/0vMQ/URvwz9Suv04vUAdwD20DvkjbgS/RE8CXKQB8hTqBeyXuox3Av9BTwFfpaf01eg34Kr1OzwD/SjuBb9Au/S/0psS36Fng27Qb+A79EfiuxPfoOeD79DxwP/1J30cHJB6kF/S99AHtAR6iF4GHJR6hl4B/o5eBH9IrwI9on/4KHZX4Mf0F+Hd6VX+Z/kGvAf8p8Ri9DvyE3tBfouP0JvCExE/pLeBn9DbwX/QO8HOJX9B7+ot0kt4Hfkn7gV8B99DXdAD4DR0EfksfAL+TeIoO6y9QFx0B6vQ34H9z+n8+p3/6M8/p//i3c/rHP5LTPz4npx/9kZz+0Tk5/cN/I6cfOZ3TF/fI6Yd/JKcfljn98Dk5/ZDM6YfOyumHZE4/JHP6obNy+gfn5PSDMqcflDn94M8wp7/9/yinv/7fnP7fnP6zy+k/9+f0n29O/7Hn9P/m9P/m9B/O6X/++ef0/wVVj3DwCmVuZHN0cmVhbQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFBQStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYKL1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","mediaType":"application/pdf","fileName":"renamed.pdf","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"seconds":0,"nanos":30000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"56","testCaseId":"48","timestamp":{"seconds":0,"nanos":31000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"seconds":0,"nanos":32000000}}} +{"attachment":{"testCaseStartedId":"56","testStepId":"49","body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list","timestamp":{"seconds":0,"nanos":33000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":34000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":35000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"35","timestamp":{"seconds":0,"nanos":36000000},"success":true}} diff --git a/testdata/src/backgrounds.ndjson b/testdata/src/backgrounds.ndjson new file mode 100644 index 0000000..311b3ae --- /dev/null +++ b/testdata/src/backgrounds.ndjson @@ -0,0 +1,36 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Backgrounds\n Though not recommended, Backgrounds can be used to share context steps between Scenarios. The Background steps\n are prepended to the steps in each Scenario when they are compiled to Pickles. Only one Background at the Feature\n level is supported.\n\n Background:\n Given an order for \"eggs\"\n And an order for \"milk\"\n And an order for \"bread\"\n\n Scenario: one scenario\n When an action\n Then an outcome\n\n Scenario: another scenario\n When an action\n Then an outcome","uri":"samples/backgrounds/backgrounds.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Backgrounds","description":" Though not recommended, Backgrounds can be used to share context steps between Scenarios. The Background steps\n are prepended to the steps in each Scenario when they are compiled to Pickles. Only one Background at the Feature\n level is supported.","children":[{"background":{"id":"3","location":{"line":6,"column":3},"keyword":"Background","name":"","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""},{"id":"1","location":{"line":8,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"milk\""},{"id":"2","location":{"line":9,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"bread\""}]}},{"scenario":{"id":"6","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"one scenario","description":"","steps":[{"id":"4","location":{"line":12,"column":5},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"5","location":{"line":13,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":15,"column":3},"keyword":"Scenario","name":"another scenario","description":"","steps":[{"id":"7","location":{"line":16,"column":5},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"8","location":{"line":17,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}}]},"comments":[],"uri":"samples/backgrounds/backgrounds.feature"}} +{"pickle":{"id":"15","uri":"samples/backgrounds/backgrounds.feature","astNodeIds":["6"],"tags":[],"name":"one scenario","language":"en","steps":[{"id":"10","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"11","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"12","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"13","text":"an action","type":"Action","astNodeIds":["4"]},{"id":"14","text":"an outcome","type":"Outcome","astNodeIds":["5"]}]}} +{"pickle":{"id":"21","uri":"samples/backgrounds/backgrounds.feature","astNodeIds":["9"],"tags":[],"name":"another scenario","language":"en","steps":[{"id":"16","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"17","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"18","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"19","text":"an action","type":"Action","astNodeIds":["7"]},{"id":"20","text":"an outcome","type":"Outcome","astNodeIds":["8"]}]}} +{"stepDefinition":{"id":"22","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/backgrounds/backgrounds.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"23","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an action"},"sourceReference":{"uri":"samples/backgrounds/backgrounds.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"24","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an outcome"},"sourceReference":{"uri":"samples/backgrounds/backgrounds.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"25","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"26","pickleId":"15","testSteps":[{"id":"27","pickleStepId":"10","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"28","pickleStepId":"11","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"29","pickleStepId":"12","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"30","pickleStepId":"13","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"14","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"25"}} +{"testCase":{"id":"32","pickleId":"21","testSteps":[{"id":"33","pickleStepId":"16","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"34","pickleStepId":"17","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"35","pickleStepId":"18","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"19","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"37","pickleStepId":"20","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"25"}} +{"testCaseStarted":{"id":"38","testCaseId":"26","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"27","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"27","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"28","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"28","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"29","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"29","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"30","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"31","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"31","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"38","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"39","testCaseId":"32","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"33","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"33","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"34","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"34","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"35","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"36","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"37","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"39","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"25","timestamp":{"seconds":0,"nanos":25000000},"success":true}} diff --git a/testdata/src/cdata.ndjson b/testdata/src/cdata.ndjson new file mode 100644 index 0000000..5c2d6d2 --- /dev/null +++ b/testdata/src/cdata.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: cdata\n Cucumber xml formatters should be able to handle xml cdata elements.\n\n Scenario: cdata\n Given I have 42 in my belly\n","uri":"samples/cdata/cdata.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"cdata","description":" Cucumber xml formatters should be able to handle xml cdata elements.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"cdata","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"Given ","keywordType":"Context","text":"I have 42 in my belly"}],"examples":[]}}]},"comments":[],"uri":"samples/cdata/cdata.feature"}} +{"pickle":{"id":"3","uri":"samples/cdata/cdata.feature","astNodeIds":["1"],"tags":[],"name":"cdata","language":"en","steps":[{"id":"2","text":"I have 42 in my belly","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I have {int} in my belly"},"sourceReference":{"uri":"samples/cdata/cdata.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":7,"value":"42","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/testdata/src/data-tables.ndjson b/testdata/src/data-tables.ndjson new file mode 100644 index 0000000..1b43475 --- /dev/null +++ b/testdata/src/data-tables.ndjson @@ -0,0 +1,15 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Data Tables\n Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.\n\n Scenario: transposed table\n When the following table is transposed:\n | a | b |\n | 1 | 2 |\n Then it should be:\n | a | 1 |\n | b | 2 |\n","uri":"samples/data-tables/data-tables.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Data Tables","description":" Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.","children":[{"scenario":{"id":"6","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"transposed table","description":"","steps":[{"id":"2","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"the following table is transposed:","dataTable":{"location":{"line":9,"column":7},"rows":[{"id":"0","location":{"line":9,"column":7},"cells":[{"location":{"line":9,"column":9},"value":"a"},{"location":{"line":9,"column":13},"value":"b"}]},{"id":"1","location":{"line":10,"column":7},"cells":[{"location":{"line":10,"column":9},"value":"1"},{"location":{"line":10,"column":13},"value":"2"}]}]}},{"id":"5","location":{"line":11,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"it should be:","dataTable":{"location":{"line":12,"column":7},"rows":[{"id":"3","location":{"line":12,"column":7},"cells":[{"location":{"line":12,"column":9},"value":"a"},{"location":{"line":12,"column":13},"value":"1"}]},{"id":"4","location":{"line":13,"column":7},"cells":[{"location":{"line":13,"column":9},"value":"b"},{"location":{"line":13,"column":13},"value":"2"}]}]}}],"examples":[]}}]},"comments":[],"uri":"samples/data-tables/data-tables.feature"}} +{"pickle":{"id":"9","uri":"samples/data-tables/data-tables.feature","astNodeIds":["6"],"tags":[],"name":"transposed table","language":"en","steps":[{"id":"7","text":"the following table is transposed:","type":"Action","argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"b"}]},{"cells":[{"value":"1"},{"value":"2"}]}]}},"astNodeIds":["2"]},{"id":"8","text":"it should be:","type":"Outcome","argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"1"}]},{"cells":[{"value":"b"},{"value":"2"}]}]}},"astNodeIds":["5"]}]}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the following table is transposed:"},"sourceReference":{"uri":"samples/data-tables/data-tables.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"11","pattern":{"type":"CUCUMBER_EXPRESSION","source":"it should be:"},"sourceReference":{"uri":"samples/data-tables/data-tables.ts","location":{"line":8}}}} +{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"13","pickleId":"9","testSteps":[{"id":"14","pickleStepId":"7","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"15","pickleStepId":"8","stepDefinitionIds":["11"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"12"}} +{"testCaseStarted":{"id":"16","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"16","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"16","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"16","testStepId":"15","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"16","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testCaseFinished":{"testCaseStartedId":"16","timestamp":{"seconds":0,"nanos":6000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":7000000},"success":true}} diff --git a/testdata/src/doc-strings.ndjson b/testdata/src/doc-strings.ndjson new file mode 100644 index 0000000..f529b32 --- /dev/null +++ b/testdata/src/doc-strings.ndjson @@ -0,0 +1,24 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Doc strings\n Doc strings are a way to supply long, sometimes multi-line, text to a step. They are passed as the last argument\n to the step definition.\n\n Scenario: a doc string with standard delimiter\n Three double quotes above and below are the standard delimiter for doc strings.\n\n Given a doc string:\n \"\"\"\n Here is some content\n And some more on another line\n \"\"\"\n\n Scenario: a doc string with backticks delimiter\n Backticks can also be used, like Markdown, but are less widely supported by editors.\n\n Given a doc string:\n ```\n Here is some content\n And some more on another line\n ```\n\n Scenario: a doc string with media type\n The media type can be optionally specified too, following the opening delimiter.\n\n Given a doc string:\n \"\"\"application/json\n {\n \"foo\": \"bar\"\n }\n \"\"\"","uri":"samples/doc-strings/doc-strings.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Doc strings","description":" Doc strings are a way to supply long, sometimes multi-line, text to a step. They are passed as the last argument\n to the step definition.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"a doc string with standard delimiter","description":" Three double quotes above and below are the standard delimiter for doc strings.","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"a doc string:","docString":{"location":{"line":9,"column":5},"content":"Here is some content\nAnd some more on another line","delimiter":"\"\"\""}}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":14,"column":3},"keyword":"Scenario","name":"a doc string with backticks delimiter","description":" Backticks can also be used, like Markdown, but are less widely supported by editors.","steps":[{"id":"2","location":{"line":17,"column":5},"keyword":"Given ","keywordType":"Context","text":"a doc string:","docString":{"location":{"line":18,"column":5},"content":"Here is some content\nAnd some more on another line","delimiter":"```"}}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":23,"column":3},"keyword":"Scenario","name":"a doc string with media type","description":" The media type can be optionally specified too, following the opening delimiter.","steps":[{"id":"4","location":{"line":26,"column":5},"keyword":"Given ","keywordType":"Context","text":"a doc string:","docString":{"location":{"line":27,"column":5},"content":"{\n \"foo\": \"bar\"\n}","delimiter":"\"\"\"","mediaType":"application/json"}}],"examples":[]}}]},"comments":[],"uri":"samples/doc-strings/doc-strings.feature"}} +{"pickle":{"id":"7","uri":"samples/doc-strings/doc-strings.feature","astNodeIds":["1"],"tags":[],"name":"a doc string with standard delimiter","language":"en","steps":[{"id":"6","text":"a doc string:","type":"Context","argument":{"docString":{"content":"Here is some content\nAnd some more on another line"}},"astNodeIds":["0"]}]}} +{"pickle":{"id":"9","uri":"samples/doc-strings/doc-strings.feature","astNodeIds":["3"],"tags":[],"name":"a doc string with backticks delimiter","language":"en","steps":[{"id":"8","text":"a doc string:","type":"Context","argument":{"docString":{"content":"Here is some content\nAnd some more on another line"}},"astNodeIds":["2"]}]}} +{"pickle":{"id":"11","uri":"samples/doc-strings/doc-strings.feature","astNodeIds":["5"],"tags":[],"name":"a doc string with media type","language":"en","steps":[{"id":"10","text":"a doc string:","type":"Context","argument":{"docString":{"content":"{\n \"foo\": \"bar\"\n}","mediaType":"application/json"}},"astNodeIds":["4"]}]}} +{"stepDefinition":{"id":"12","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a doc string:"},"sourceReference":{"uri":"samples/doc-strings/doc-strings.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"13","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"14","pickleId":"7","testSteps":[{"id":"15","pickleStepId":"6","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"13"}} +{"testCase":{"id":"16","pickleId":"9","testSteps":[{"id":"17","pickleStepId":"8","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"13"}} +{"testCase":{"id":"18","pickleId":"11","testSteps":[{"id":"19","pickleStepId":"10","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"13"}} +{"testCaseStarted":{"id":"20","testCaseId":"14","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"20","testStepId":"15","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"20","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"20","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"21","testCaseId":"16","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"17","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"17","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"22","testCaseId":"18","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"19","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"19","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"13","timestamp":{"seconds":0,"nanos":13000000},"success":true}} diff --git a/testdata/src/empty.ndjson b/testdata/src/empty.ndjson new file mode 100644 index 0000000..7aefb40 --- /dev/null +++ b/testdata/src/empty.ndjson @@ -0,0 +1,9 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Empty Scenarios\n Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is.\n\n Scenario: Blank Scenario\n","uri":"samples/empty/empty.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Empty Scenarios","description":" Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is.","children":[{"scenario":{"id":"0","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"Blank Scenario","description":"","steps":[],"examples":[]}}]},"comments":[],"uri":"samples/empty/empty.feature"}} +{"pickle":{"id":"1","uri":"samples/empty/empty.feature","astNodeIds":["0"],"tags":[],"name":"Blank Scenario","language":"en","steps":[]}} +{"testRunStarted":{"id":"2","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"3","pickleId":"1","testSteps":[],"testRunStartedId":"2"}} +{"testCaseStarted":{"id":"4","testCaseId":"3","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testCaseFinished":{"testCaseStartedId":"4","timestamp":{"seconds":0,"nanos":2000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"2","timestamp":{"seconds":0,"nanos":3000000},"success":true}} diff --git a/testdata/src/examples-tables-attachment.ndjson b/testdata/src/examples-tables-attachment.ndjson new file mode 100644 index 0000000..ad29127 --- /dev/null +++ b/testdata/src/examples-tables-attachment.ndjson @@ -0,0 +1,21 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Examples Tables - With attachments\n It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Scenario Outline: Attaching images in an examples table\n When a image is attached\n\n Examples:\n | type |\n | JPEG |\n | PNG |\n","uri":"samples/examples-tables-attachment/examples-tables-attachment.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Examples Tables - With attachments","description":" It is sometimes useful to take a screenshot while a scenario runs or capture some logs.","children":[{"scenario":{"id":"5","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario Outline","name":"Attaching images in an examples table","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a image is attached"}],"examples":[{"id":"4","tags":[],"location":{"line":7,"column":5},"keyword":"Examples","name":"","description":"","tableHeader":{"id":"1","location":{"line":8,"column":7},"cells":[{"location":{"line":8,"column":9},"value":"type"}]},"tableBody":[{"id":"2","location":{"line":9,"column":7},"cells":[{"location":{"line":9,"column":9},"value":"JPEG"}]},{"id":"3","location":{"line":10,"column":7},"cells":[{"location":{"line":10,"column":9},"value":"PNG"}]}]}]}}]},"comments":[],"uri":"samples/examples-tables-attachment/examples-tables-attachment.feature"}} +{"pickle":{"id":"7","uri":"samples/examples-tables-attachment/examples-tables-attachment.feature","astNodeIds":["5","2"],"name":"Attaching images in an examples table","language":"en","steps":[{"id":"6","text":"a JPEG image is attached","type":"Action","astNodeIds":["0","2"]}],"tags":[]}} +{"pickle":{"id":"9","uri":"samples/examples-tables-attachment/examples-tables-attachment.feature","astNodeIds":["5","3"],"name":"Attaching images in an examples table","language":"en","steps":[{"id":"8","text":"a PNG image is attached","type":"Action","astNodeIds":["0","3"]}],"tags":[]}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a JPEG image is attached"},"sourceReference":{"uri":"samples/examples-tables-attachment/examples-tables-attachment.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"11","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a PNG image is attached"},"sourceReference":{"uri":"samples/examples-tables-attachment/examples-tables-attachment.ts","location":{"line":8}}}} +{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"13","pickleId":"7","testSteps":[{"id":"14","pickleStepId":"6","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"12"}} +{"testCase":{"id":"15","pickleId":"9","testSteps":[{"id":"16","pickleStepId":"8","stepDefinitionIds":["11"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"12"}} +{"testCaseStarted":{"id":"17","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"17","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}} +{"attachment":{"testCaseStartedId":"17","testStepId":"14","body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepFinished":{"testCaseStartedId":"17","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":4000000}}} +{"testCaseFinished":{"testCaseStartedId":"17","timestamp":{"seconds":0,"nanos":5000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"18","testCaseId":"15","timestamp":{"seconds":0,"nanos":6000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"18","testStepId":"16","timestamp":{"seconds":0,"nanos":7000000}}} +{"attachment":{"testCaseStartedId":"18","testStepId":"16","body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"18","testStepId":"16","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"18","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":11000000},"success":true}} diff --git a/testdata/src/examples-tables-undefined.ndjson b/testdata/src/examples-tables-undefined.ndjson new file mode 100644 index 0000000..df0e7b2 --- /dev/null +++ b/testdata/src/examples-tables-undefined.ndjson @@ -0,0 +1,41 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Examples Tables - With Undefined Steps\n The replacement pattern used in scenario outlines does not influence how steps\n are matched. The replacement pattern is replaced, and step definitions are\n matched against that text. Because of that the following results in one\n undefined step for each example and a suggested snippet to implement it. \n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @undefined\n Examples: These are undefined because the value is not an {int}\n | start | eat | left |\n | pear | 1 | 12 |\n | 12 | banana | 12 |\n | 0 | 1 | apple |\n","uri":"samples/examples-tables-undefined/examples-undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Examples Tables - With Undefined Steps","description":" The replacement pattern used in scenario outlines does not influence how steps\n are matched. The replacement pattern is replaced, and step definitions are\n matched against that text. Because of that the following results in one\n undefined step for each example and a suggested snippet to implement it. ","children":[{"scenario":{"id":"9","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario Outline","name":"Eating cucumbers","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"there are cucumbers"},{"id":"1","location":{"line":9,"column":5},"keyword":"When ","keywordType":"Action","text":"I eat cucumbers"},{"id":"2","location":{"line":10,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"I should have cucumbers"}],"examples":[{"id":"8","tags":[{"location":{"line":12,"column":5},"name":"@undefined","id":"7"}],"location":{"line":13,"column":5},"keyword":"Examples","name":"These are undefined because the value is not an {int}","description":"","tableHeader":{"id":"3","location":{"line":14,"column":7},"cells":[{"location":{"line":14,"column":9},"value":"start"},{"location":{"line":14,"column":17},"value":"eat"},{"location":{"line":14,"column":26},"value":"left"}]},"tableBody":[{"id":"4","location":{"line":15,"column":7},"cells":[{"location":{"line":15,"column":9},"value":"pear"},{"location":{"line":15,"column":17},"value":"1"},{"location":{"line":15,"column":26},"value":"12"}]},{"id":"5","location":{"line":16,"column":7},"cells":[{"location":{"line":16,"column":9},"value":"12"},{"location":{"line":16,"column":17},"value":"banana"},{"location":{"line":16,"column":26},"value":"12"}]},{"id":"6","location":{"line":17,"column":7},"cells":[{"location":{"line":17,"column":9},"value":"0"},{"location":{"line":17,"column":17},"value":"1"},{"location":{"line":17,"column":26},"value":"apple"}]}]}]}}]},"comments":[],"uri":"samples/examples-tables-undefined/examples-undefined.feature"}} +{"pickle":{"id":"13","uri":"samples/examples-tables-undefined/examples-undefined.feature","astNodeIds":["9","4"],"name":"Eating cucumbers","language":"en","steps":[{"id":"10","text":"there are pear cucumbers","type":"Context","astNodeIds":["0","4"]},{"id":"11","text":"I eat 1 cucumbers","type":"Action","astNodeIds":["1","4"]},{"id":"12","text":"I should have 12 cucumbers","type":"Outcome","astNodeIds":["2","4"]}],"tags":[{"name":"@undefined","astNodeId":"7"}]}} +{"pickle":{"id":"17","uri":"samples/examples-tables-undefined/examples-undefined.feature","astNodeIds":["9","5"],"name":"Eating cucumbers","language":"en","steps":[{"id":"14","text":"there are 12 cucumbers","type":"Context","astNodeIds":["0","5"]},{"id":"15","text":"I eat banana cucumbers","type":"Action","astNodeIds":["1","5"]},{"id":"16","text":"I should have 12 cucumbers","type":"Outcome","astNodeIds":["2","5"]}],"tags":[{"name":"@undefined","astNodeId":"7"}]}} +{"pickle":{"id":"21","uri":"samples/examples-tables-undefined/examples-undefined.feature","astNodeIds":["9","6"],"name":"Eating cucumbers","language":"en","steps":[{"id":"18","text":"there are 0 cucumbers","type":"Context","astNodeIds":["0","6"]},{"id":"19","text":"I eat 1 cucumbers","type":"Action","astNodeIds":["1","6"]},{"id":"20","text":"I should have apple cucumbers","type":"Outcome","astNodeIds":["2","6"]}],"tags":[{"name":"@undefined","astNodeId":"7"}]}} +{"stepDefinition":{"id":"22","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables-undefined/examples-tables-undefined.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"23","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I eat {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables-undefined/examples-tables-undefined.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"24","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I should have {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables-undefined/examples-tables-undefined.ts","location":{"line":12}}}} +{"testRunStarted":{"id":"25","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"26","pickleId":"13","testSteps":[{"id":"27","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"28","pickleStepId":"11","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"29","pickleStepId":"12","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"12","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"25"}} +{"testCase":{"id":"30","pickleId":"17","testSteps":[{"id":"31","pickleStepId":"14","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"32","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"33","pickleStepId":"16","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"12","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"25"}} +{"testCase":{"id":"34","pickleId":"21","testSteps":[{"id":"35","pickleStepId":"18","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"0","children":[]},"parameterTypeName":"int"}]}]},{"id":"36","pickleStepId":"19","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"37","pickleStepId":"20","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"25"}} +{"testCaseStarted":{"id":"38","testCaseId":"26","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"27","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"39","pickleStepId":"10","snippets":[{"language":"typescript","code":"Given(\"there are pear cucumbers\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"27","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"28","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"28","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"29","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"29","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"38","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"40","testCaseId":"30","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"31","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"31","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"32","timestamp":{"seconds":0,"nanos":12000000}}} +{"suggestion":{"id":"41","pickleStepId":"15","snippets":[{"language":"typescript","code":"When(\"I eat banana cucumbers\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"32","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"33","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"33","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"40","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"42","testCaseId":"34","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"42","testStepId":"35","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"42","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"42","testStepId":"36","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"42","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"42","testStepId":"37","timestamp":{"seconds":0,"nanos":22000000}}} +{"suggestion":{"id":"43","pickleStepId":"20","snippets":[{"language":"typescript","code":"Then(\"I should have apple cucumbers\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"42","testStepId":"37","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"42","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"25","timestamp":{"seconds":0,"nanos":25000000},"success":false}} diff --git a/testdata/src/examples-tables.ndjson b/testdata/src/examples-tables.ndjson new file mode 100644 index 0000000..3207ee0 --- /dev/null +++ b/testdata/src/examples-tables.ndjson @@ -0,0 +1,80 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","uri":"samples/examples-tables/examples-tables.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Examples Tables","description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","children":[{"scenario":{"id":"13","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario Outline","name":"Eating cucumbers","description":"","steps":[{"id":"0","location":{"line":12,"column":5},"keyword":"Given ","keywordType":"Context","text":"there are cucumbers"},{"id":"1","location":{"line":13,"column":5},"keyword":"When ","keywordType":"Action","text":"I eat cucumbers"},{"id":"2","location":{"line":14,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"I should have cucumbers"}],"examples":[{"id":"7","tags":[{"location":{"line":16,"column":5},"name":"@passing","id":"6"}],"location":{"line":17,"column":5},"keyword":"Examples","name":"These are passing","description":"","tableHeader":{"id":"3","location":{"line":18,"column":7},"cells":[{"location":{"line":18,"column":9},"value":"start"},{"location":{"line":18,"column":17},"value":"eat"},{"location":{"line":18,"column":23},"value":"left"}]},"tableBody":[{"id":"4","location":{"line":19,"column":7},"cells":[{"location":{"line":19,"column":12},"value":"12"},{"location":{"line":19,"column":19},"value":"5"},{"location":{"line":19,"column":26},"value":"7"}]},{"id":"5","location":{"line":20,"column":7},"cells":[{"location":{"line":20,"column":12},"value":"20"},{"location":{"line":20,"column":19},"value":"5"},{"location":{"line":20,"column":25},"value":"15"}]}]},{"id":"12","tags":[{"location":{"line":22,"column":5},"name":"@failing","id":"11"}],"location":{"line":23,"column":5},"keyword":"Examples","name":"These are failing","description":"","tableHeader":{"id":"8","location":{"line":24,"column":7},"cells":[{"location":{"line":24,"column":9},"value":"start"},{"location":{"line":24,"column":17},"value":"eat"},{"location":{"line":24,"column":23},"value":"left"}]},"tableBody":[{"id":"9","location":{"line":25,"column":7},"cells":[{"location":{"line":25,"column":12},"value":"12"},{"location":{"line":25,"column":18},"value":"20"},{"location":{"line":25,"column":26},"value":"0"}]},{"id":"10","location":{"line":26,"column":7},"cells":[{"location":{"line":26,"column":13},"value":"0"},{"location":{"line":26,"column":19},"value":"1"},{"location":{"line":26,"column":26},"value":"0"}]}]}]}},{"scenario":{"id":"22","tags":[],"location":{"line":28,"column":3},"keyword":"Scenario Outline","name":"Eating cucumbers with friends","description":"","steps":[{"id":"14","location":{"line":29,"column":5},"keyword":"Given ","keywordType":"Context","text":"there are friends"},{"id":"15","location":{"line":30,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"there are cucumbers"},{"id":"16","location":{"line":31,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"each person can eat cucumbers"}],"examples":[{"id":"21","tags":[],"location":{"line":33,"column":5},"keyword":"Examples","name":"","description":"","tableHeader":{"id":"17","location":{"line":34,"column":7},"cells":[{"location":{"line":34,"column":9},"value":"friends"},{"location":{"line":34,"column":19},"value":"start"},{"location":{"line":34,"column":27},"value":"share"}]},"tableBody":[{"id":"18","location":{"line":35,"column":7},"cells":[{"location":{"line":35,"column":14},"value":"11"},{"location":{"line":35,"column":22},"value":"12"},{"location":{"line":35,"column":31},"value":"1"}]},{"id":"19","location":{"line":36,"column":7},"cells":[{"location":{"line":36,"column":15},"value":"1"},{"location":{"line":36,"column":23},"value":"4"},{"location":{"line":36,"column":31},"value":"2"}]},{"id":"20","location":{"line":37,"column":7},"cells":[{"location":{"line":37,"column":15},"value":"0"},{"location":{"line":37,"column":23},"value":"4"},{"location":{"line":37,"column":31},"value":"4"}]}]}]}}]},"comments":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"id":"26","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["13","4"],"name":"Eating cucumbers","language":"en","steps":[{"id":"23","text":"there are 12 cucumbers","type":"Context","astNodeIds":["0","4"]},{"id":"24","text":"I eat 5 cucumbers","type":"Action","astNodeIds":["1","4"]},{"id":"25","text":"I should have 7 cucumbers","type":"Outcome","astNodeIds":["2","4"]}],"tags":[{"name":"@passing","astNodeId":"6"}]}} +{"pickle":{"id":"30","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["13","5"],"name":"Eating cucumbers","language":"en","steps":[{"id":"27","text":"there are 20 cucumbers","type":"Context","astNodeIds":["0","5"]},{"id":"28","text":"I eat 5 cucumbers","type":"Action","astNodeIds":["1","5"]},{"id":"29","text":"I should have 15 cucumbers","type":"Outcome","astNodeIds":["2","5"]}],"tags":[{"name":"@passing","astNodeId":"6"}]}} +{"pickle":{"id":"34","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["13","9"],"name":"Eating cucumbers","language":"en","steps":[{"id":"31","text":"there are 12 cucumbers","type":"Context","astNodeIds":["0","9"]},{"id":"32","text":"I eat 20 cucumbers","type":"Action","astNodeIds":["1","9"]},{"id":"33","text":"I should have 0 cucumbers","type":"Outcome","astNodeIds":["2","9"]}],"tags":[{"name":"@failing","astNodeId":"11"}]}} +{"pickle":{"id":"38","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["13","10"],"name":"Eating cucumbers","language":"en","steps":[{"id":"35","text":"there are 0 cucumbers","type":"Context","astNodeIds":["0","10"]},{"id":"36","text":"I eat 1 cucumbers","type":"Action","astNodeIds":["1","10"]},{"id":"37","text":"I should have 0 cucumbers","type":"Outcome","astNodeIds":["2","10"]}],"tags":[{"name":"@failing","astNodeId":"11"}]}} +{"pickle":{"id":"42","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["22","18"],"name":"Eating cucumbers with 11 friends","language":"en","steps":[{"id":"39","text":"there are 11 friends","type":"Context","astNodeIds":["14","18"]},{"id":"40","text":"there are 12 cucumbers","type":"Context","astNodeIds":["15","18"]},{"id":"41","text":"each person can eat 1 cucumbers","type":"Outcome","astNodeIds":["16","18"]}],"tags":[]}} +{"pickle":{"id":"46","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["22","19"],"name":"Eating cucumbers with 1 friends","language":"en","steps":[{"id":"43","text":"there are 1 friends","type":"Context","astNodeIds":["14","19"]},{"id":"44","text":"there are 4 cucumbers","type":"Context","astNodeIds":["15","19"]},{"id":"45","text":"each person can eat 2 cucumbers","type":"Outcome","astNodeIds":["16","19"]}],"tags":[]}} +{"pickle":{"id":"50","uri":"samples/examples-tables/examples-tables.feature","astNodeIds":["22","20"],"name":"Eating cucumbers with 0 friends","language":"en","steps":[{"id":"47","text":"there are 0 friends","type":"Context","astNodeIds":["14","20"]},{"id":"48","text":"there are 4 cucumbers","type":"Context","astNodeIds":["15","20"]},{"id":"49","text":"each person can eat 4 cucumbers","type":"Outcome","astNodeIds":["16","20"]}],"tags":[]}} +{"stepDefinition":{"id":"51","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"52","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are {int} friends"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"53","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I eat {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"54","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I should have {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":16}}}} +{"stepDefinition":{"id":"55","pattern":{"type":"CUCUMBER_EXPRESSION","source":"each person can eat {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":20}}}} +{"testRunStarted":{"id":"56","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"57","pickleId":"26","testSteps":[{"id":"58","pickleStepId":"23","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"24","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"5","children":[]},"parameterTypeName":"int"}]}]},{"id":"60","pickleStepId":"25","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"7","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"61","pickleId":"30","testSteps":[{"id":"62","pickleStepId":"27","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"20","children":[]},"parameterTypeName":"int"}]}]},{"id":"63","pickleStepId":"28","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"5","children":[]},"parameterTypeName":"int"}]}]},{"id":"64","pickleStepId":"29","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"15","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"65","pickleId":"34","testSteps":[{"id":"66","pickleStepId":"31","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"67","pickleStepId":"32","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"20","children":[]},"parameterTypeName":"int"}]}]},{"id":"68","pickleStepId":"33","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"0","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"69","pickleId":"38","testSteps":[{"id":"70","pickleStepId":"35","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"0","children":[]},"parameterTypeName":"int"}]}]},{"id":"71","pickleStepId":"36","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"72","pickleStepId":"37","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"0","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"73","pickleId":"42","testSteps":[{"id":"74","pickleStepId":"39","stepDefinitionIds":["52"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"11","children":[]},"parameterTypeName":"int"}]}]},{"id":"75","pickleStepId":"40","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"76","pickleStepId":"41","stepDefinitionIds":["55"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":20,"value":"1","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"77","pickleId":"46","testSteps":[{"id":"78","pickleStepId":"43","stepDefinitionIds":["52"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"79","pickleStepId":"44","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"4","children":[]},"parameterTypeName":"int"}]}]},{"id":"80","pickleStepId":"45","stepDefinitionIds":["55"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":20,"value":"2","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"81","pickleId":"50","testSteps":[{"id":"82","pickleStepId":"47","stepDefinitionIds":["52"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"0","children":[]},"parameterTypeName":"int"}]}]},{"id":"83","pickleStepId":"48","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"4","children":[]},"parameterTypeName":"int"}]}]},{"id":"84","pickleStepId":"49","stepDefinitionIds":["55"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":20,"value":"4","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCaseStarted":{"id":"85","testCaseId":"57","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"58","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"58","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"59","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"59","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"60","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"60","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"85","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"86","testCaseId":"61","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"62","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"62","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"63","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"63","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"64","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"64","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"86","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"87","testCaseId":"65","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"66","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"66","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"67","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"67","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"68","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"68","testStepResult":{"message":"Expected values to be strictly equal:\n\n-8 !== 0\n","exception":{"type":"AssertionError","message":"Expected values to be strictly equal:\n\n-8 !== 0\n","stackTrace":"AssertionError: Expected values to be strictly equal:\n\n-8 !== 0\n\nsamples/examples-tables/examples-tables.feature:14"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"87","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"88","testCaseId":"69","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"70","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"70","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"71","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"71","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"72","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"72","testStepResult":{"message":"Expected values to be strictly equal:\n\n-1 !== 0\n","exception":{"type":"AssertionError","message":"Expected values to be strictly equal:\n\n-1 !== 0\n","stackTrace":"AssertionError: Expected values to be strictly equal:\n\n-1 !== 0\n\nsamples/examples-tables/examples-tables.feature:14"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"88","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"89","testCaseId":"73","timestamp":{"seconds":0,"nanos":33000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"89","testStepId":"74","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"89","testStepId":"74","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testStepStarted":{"testCaseStartedId":"89","testStepId":"75","timestamp":{"seconds":0,"nanos":36000000}}} +{"testStepFinished":{"testCaseStartedId":"89","testStepId":"75","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":37000000}}} +{"testStepStarted":{"testCaseStartedId":"89","testStepId":"76","timestamp":{"seconds":0,"nanos":38000000}}} +{"testStepFinished":{"testCaseStartedId":"89","testStepId":"76","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":39000000}}} +{"testCaseFinished":{"testCaseStartedId":"89","timestamp":{"seconds":0,"nanos":40000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"90","testCaseId":"77","timestamp":{"seconds":0,"nanos":41000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"90","testStepId":"78","timestamp":{"seconds":0,"nanos":42000000}}} +{"testStepFinished":{"testCaseStartedId":"90","testStepId":"78","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":43000000}}} +{"testStepStarted":{"testCaseStartedId":"90","testStepId":"79","timestamp":{"seconds":0,"nanos":44000000}}} +{"testStepFinished":{"testCaseStartedId":"90","testStepId":"79","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":45000000}}} +{"testStepStarted":{"testCaseStartedId":"90","testStepId":"80","timestamp":{"seconds":0,"nanos":46000000}}} +{"testStepFinished":{"testCaseStartedId":"90","testStepId":"80","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":47000000}}} +{"testCaseFinished":{"testCaseStartedId":"90","timestamp":{"seconds":0,"nanos":48000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"91","testCaseId":"81","timestamp":{"seconds":0,"nanos":49000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"91","testStepId":"82","timestamp":{"seconds":0,"nanos":50000000}}} +{"testStepFinished":{"testCaseStartedId":"91","testStepId":"82","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":51000000}}} +{"testStepStarted":{"testCaseStartedId":"91","testStepId":"83","timestamp":{"seconds":0,"nanos":52000000}}} +{"testStepFinished":{"testCaseStartedId":"91","testStepId":"83","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":53000000}}} +{"testStepStarted":{"testCaseStartedId":"91","testStepId":"84","timestamp":{"seconds":0,"nanos":54000000}}} +{"testStepFinished":{"testCaseStartedId":"91","testStepId":"84","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":55000000}}} +{"testCaseFinished":{"testCaseStartedId":"91","timestamp":{"seconds":0,"nanos":56000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"56","timestamp":{"seconds":0,"nanos":57000000},"success":false}} diff --git a/testdata/src/global-hooks-afterall-error.ndjson b/testdata/src/global-hooks-afterall-error.ndjson new file mode 100644 index 0000000..f3fc04d --- /dev/null +++ b/testdata/src/global-hooks-afterall-error.ndjson @@ -0,0 +1,27 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks - AfterAll error\n Errors in AfterAll hooks cause the whole test run to fail. The remaining AfterAll hooks will still run, in an\n effort to clean up resources as well as possible.\n\n Scenario: A passing scenario\n When a step passes\n","uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks - AfterAll error","description":" Errors in AfterAll hooks cause the whole test run to fail. The remaining AfterAll hooks will still run, in an\n effort to clean up resources as well as possible.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"A passing scenario","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.feature"}} +{"pickle":{"id":"3","uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.feature","astNodeIds":["1"],"tags":[],"name":"A passing scenario","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"6","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":11}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":3}}}} +{"hook":{"id":"5","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":7}}}} +{"hook":{"id":"7","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":15}}}} +{"hook":{"id":"8","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":19}}}} +{"hook":{"id":"9","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"10","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"11","hookId":"4","timestamp":{"seconds":0,"nanos":1000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"11","timestamp":{"seconds":0,"nanos":2000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"12","hookId":"5","timestamp":{"seconds":0,"nanos":3000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"12","timestamp":{"seconds":0,"nanos":4000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testCase":{"id":"13","pickleId":"3","testSteps":[{"id":"14","pickleStepId":"2","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"10"}} +{"testCaseStarted":{"id":"15","testCaseId":"13","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"15","testStepId":"14","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"15","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"15","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"16","hookId":"9","timestamp":{"seconds":0,"nanos":9000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"16","timestamp":{"seconds":0,"nanos":10000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"17","hookId":"8","timestamp":{"seconds":0,"nanos":11000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"17","timestamp":{"seconds":0,"nanos":12000000},"result":{"message":"AfterAll hook went wrong","exception":{"type":"Error","message":"AfterAll hook went wrong","stackTrace":"Error: AfterAll hook went wrong\nsamples/global-hooks-afterall-error/global-hooks-afterall-error.ts:19"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"18","hookId":"7","timestamp":{"seconds":0,"nanos":13000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"18","timestamp":{"seconds":0,"nanos":14000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"10","timestamp":{"seconds":0,"nanos":15000000},"success":false}} diff --git a/testdata/src/global-hooks-attachments.ndjson b/testdata/src/global-hooks-attachments.ndjson new file mode 100644 index 0000000..dfc8e17 --- /dev/null +++ b/testdata/src/global-hooks-attachments.ndjson @@ -0,0 +1,20 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks with attachments\n Attachments can be captured in BeforeAll and AfterAll hooks.\n\n Scenario: A scenario\n When a step passes\n","uri":"samples/global-hooks-attachments/global-hooks-attachments.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks with attachments","description":" Attachments can be captured in BeforeAll and AfterAll hooks.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"A scenario","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks-attachments/global-hooks-attachments.feature"}} +{"pickle":{"id":"3","uri":"samples/global-hooks-attachments/global-hooks-attachments.feature","astNodeIds":["1"],"tags":[],"name":"A scenario","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks-attachments/global-hooks-attachments.ts","location":{"line":7}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-attachments/global-hooks-attachments.ts","location":{"line":3}}}} +{"hook":{"id":"6","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-attachments/global-hooks-attachments.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"7","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"7","id":"8","hookId":"4","timestamp":{"seconds":0,"nanos":1000000}}} +{"attachment":{"testRunHookStartedId":"8","body":"Attachment from BeforeAll hook","contentEncoding":"IDENTITY","mediaType":"text/plain","timestamp":{"seconds":0,"nanos":2000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"8","timestamp":{"seconds":0,"nanos":3000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testCase":{"id":"9","pickleId":"3","testSteps":[{"id":"10","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"7"}} +{"testCaseStarted":{"id":"11","testCaseId":"9","timestamp":{"seconds":0,"nanos":4000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"10","timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":6000000}}} +{"testCaseFinished":{"testCaseStartedId":"11","timestamp":{"seconds":0,"nanos":7000000},"willBeRetried":false}} +{"testRunHookStarted":{"testRunStartedId":"7","id":"12","hookId":"6","timestamp":{"seconds":0,"nanos":8000000}}} +{"attachment":{"testRunHookStartedId":"12","body":"Attachment from AfterAll hook","contentEncoding":"IDENTITY","mediaType":"text/plain","timestamp":{"seconds":0,"nanos":9000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"12","timestamp":{"seconds":0,"nanos":10000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"7","timestamp":{"seconds":0,"nanos":11000000},"success":true}} diff --git a/testdata/src/global-hooks-beforeall-error.ndjson b/testdata/src/global-hooks-beforeall-error.ndjson new file mode 100644 index 0000000..0785d79 --- /dev/null +++ b/testdata/src/global-hooks-beforeall-error.ndjson @@ -0,0 +1,22 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks - BeforeAll error\n Errors in BeforeAll hooks cause the whole test run to fail. Test cases will not be executed. The remaining BeforeAll\n hooks will still run, along with all AfterAll hooks, in an effort to clean up resources as well as possible.\n\n Scenario: A passing scenario\n When a step passes\n","uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks - BeforeAll error","description":" Errors in BeforeAll hooks cause the whole test run to fail. Test cases will not be executed. The remaining BeforeAll\n hooks will still run, along with all AfterAll hooks, in an effort to clean up resources as well as possible.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"A passing scenario","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.feature"}} +{"pickle":{"id":"3","uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.feature","astNodeIds":["1"],"tags":[],"name":"A passing scenario","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"7","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":15}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":3}}}} +{"hook":{"id":"5","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":7}}}} +{"hook":{"id":"6","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":11}}}} +{"hook":{"id":"8","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":19}}}} +{"hook":{"id":"9","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"10","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"11","hookId":"4","timestamp":{"seconds":0,"nanos":1000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"11","timestamp":{"seconds":0,"nanos":2000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"12","hookId":"5","timestamp":{"seconds":0,"nanos":3000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"12","timestamp":{"seconds":0,"nanos":4000000},"result":{"message":"BeforeAll hook went wrong","exception":{"type":"Error","message":"BeforeAll hook went wrong","stackTrace":"Error: BeforeAll hook went wrong\nsamples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts:7"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"13","hookId":"6","timestamp":{"seconds":0,"nanos":5000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"13","timestamp":{"seconds":0,"nanos":6000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"14","hookId":"9","timestamp":{"seconds":0,"nanos":7000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"14","timestamp":{"seconds":0,"nanos":8000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"15","hookId":"8","timestamp":{"seconds":0,"nanos":9000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"15","timestamp":{"seconds":0,"nanos":10000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"10","timestamp":{"seconds":0,"nanos":11000000},"success":false}} diff --git a/testdata/src/global-hooks.ndjson b/testdata/src/global-hooks.ndjson new file mode 100644 index 0000000..1af9114 --- /dev/null +++ b/testdata/src/global-hooks.ndjson @@ -0,0 +1,31 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks\n Hooks can be at the test run level, so they run once before or after all test cases.\n\n AfterAll hooks are executed in reverse order of definition.\n\n Scenario: A passing scenario\n When a step passes\n\n Scenario: A failing scenario\n When a step fails\n","uri":"samples/global-hooks/global-hooks.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks","description":" Hooks can be at the test run level, so they run once before or after all test cases.\n\n AfterAll hooks are executed in reverse order of definition.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"A passing scenario","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"A failing scenario","description":"","steps":[{"id":"2","location":{"line":10,"column":5},"keyword":"When ","keywordType":"Action","text":"a step fails"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks/global-hooks.feature"}} +{"pickle":{"id":"5","uri":"samples/global-hooks/global-hooks.feature","astNodeIds":["1"],"tags":[],"name":"A passing scenario","language":"en","steps":[{"id":"4","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"7","uri":"samples/global-hooks/global-hooks.feature","astNodeIds":["3"],"tags":[],"name":"A failing scenario","language":"en","steps":[{"id":"6","text":"a step fails","type":"Action","astNodeIds":["2"]}]}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":11}}}} +{"stepDefinition":{"id":"11","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step fails"},"sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":15}}}} +{"hook":{"id":"8","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":3}}}} +{"hook":{"id":"9","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":7}}}} +{"hook":{"id":"12","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":19}}}} +{"hook":{"id":"13","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"14","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"15","hookId":"8","timestamp":{"seconds":0,"nanos":1000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"15","timestamp":{"seconds":0,"nanos":2000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"16","hookId":"9","timestamp":{"seconds":0,"nanos":3000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"16","timestamp":{"seconds":0,"nanos":4000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testCase":{"id":"17","pickleId":"5","testSteps":[{"id":"18","pickleStepId":"4","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"14"}} +{"testCase":{"id":"19","pickleId":"7","testSteps":[{"id":"20","pickleStepId":"6","stepDefinitionIds":["11"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"14"}} +{"testCaseStarted":{"id":"21","testCaseId":"17","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"18","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"18","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"22","testCaseId":"19","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"20","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"20","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/global-hooks/global-hooks.feature:10"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"23","hookId":"13","timestamp":{"seconds":0,"nanos":13000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"23","timestamp":{"seconds":0,"nanos":14000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"24","hookId":"12","timestamp":{"seconds":0,"nanos":15000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"24","timestamp":{"seconds":0,"nanos":16000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"14","timestamp":{"seconds":0,"nanos":17000000},"success":false}} diff --git a/testdata/src/hooks-attachment.ndjson b/testdata/src/hooks-attachment.ndjson new file mode 100644 index 0000000..2d7c087 --- /dev/null +++ b/testdata/src/hooks-attachment.ndjson @@ -0,0 +1,20 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - Attachments\n Hooks are special steps that run before or after each scenario's steps.\n\n Like regular steps, it is possible to attach a file to the output.\n\n Scenario: With an valid attachment in the hook and a passed step\n When a step passes\n","uri":"samples/hooks-attachment/hooks-attachment.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - Attachments","description":" Hooks are special steps that run before or after each scenario's steps.\n\n Like regular steps, it is possible to attach a file to the output.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"With an valid attachment in the hook and a passed step","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-attachment/hooks-attachment.feature"}} +{"pickle":{"id":"3","uri":"samples/hooks-attachment/hooks-attachment.feature","astNodeIds":["1"],"tags":[],"name":"With an valid attachment in the hook and a passed step","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks-attachment/hooks-attachment.ts","location":{"line":9}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks-attachment/hooks-attachment.ts","location":{"line":4}}}} +{"hook":{"id":"6","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks-attachment/hooks-attachment.ts","location":{"line":13}}}} +{"testRunStarted":{"id":"7","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"8","pickleId":"3","testSteps":[{"id":"9","hookId":"4"},{"id":"10","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"11","hookId":"6"}],"testRunStartedId":"7"}} +{"testCaseStarted":{"id":"12","testCaseId":"8","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"9","timestamp":{"seconds":0,"nanos":2000000}}} +{"attachment":{"testCaseStartedId":"12","testStepId":"9","body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"9","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"10","timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"11","timestamp":{"seconds":0,"nanos":7000000}}} +{"attachment":{"testCaseStartedId":"12","testStepId":"11","body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"11","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"12","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"7","timestamp":{"seconds":0,"nanos":11000000},"success":true}} diff --git a/testdata/src/hooks-conditional.ndjson b/testdata/src/hooks-conditional.ndjson new file mode 100644 index 0000000..54723f5 --- /dev/null +++ b/testdata/src/hooks-conditional.ndjson @@ -0,0 +1,36 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - Conditional execution\n Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions.\n\n @fail-before\n Scenario: A failure in the before hook and a skipped step\n When a step passes\n\n @fail-after\n Scenario: A failure in the after hook and a passed step\n When a step passes\n\n @passing-hook\n Scenario: With an tag, a passed step and hook\n When a step passes\n","uri":"samples/hooks-conditional/hooks-conditional.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - Conditional execution","description":" Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions.","children":[{"scenario":{"id":"2","tags":[{"location":{"line":6,"column":3},"name":"@fail-before","id":"1"}],"location":{"line":7,"column":3},"keyword":"Scenario","name":"A failure in the before hook and a skipped step","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"5","tags":[{"location":{"line":10,"column":3},"name":"@fail-after","id":"4"}],"location":{"line":11,"column":3},"keyword":"Scenario","name":"A failure in the after hook and a passed step","description":"","steps":[{"id":"3","location":{"line":12,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"8","tags":[{"location":{"line":14,"column":3},"name":"@passing-hook","id":"7"}],"location":{"line":15,"column":3},"keyword":"Scenario","name":"With an tag, a passed step and hook","description":"","steps":[{"id":"6","location":{"line":16,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-conditional/hooks-conditional.feature"}} +{"pickle":{"id":"10","uri":"samples/hooks-conditional/hooks-conditional.feature","astNodeIds":["2"],"tags":[{"name":"@fail-before","astNodeId":"1"}],"name":"A failure in the before hook and a skipped step","language":"en","steps":[{"id":"9","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"12","uri":"samples/hooks-conditional/hooks-conditional.feature","astNodeIds":["5"],"tags":[{"name":"@fail-after","astNodeId":"4"}],"name":"A failure in the after hook and a passed step","language":"en","steps":[{"id":"11","text":"a step passes","type":"Action","astNodeIds":["3"]}]}} +{"pickle":{"id":"14","uri":"samples/hooks-conditional/hooks-conditional.feature","astNodeIds":["8"],"tags":[{"name":"@passing-hook","astNodeId":"7"}],"name":"With an tag, a passed step and hook","language":"en","steps":[{"id":"13","text":"a step passes","type":"Action","astNodeIds":["6"]}]}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":11}}}} +{"hook":{"id":"15","type":"BEFORE_TEST_CASE","tagExpression":"@passing-hook","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":3}}}} +{"hook":{"id":"16","type":"BEFORE_TEST_CASE","tagExpression":"@fail-before","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":7}}}} +{"hook":{"id":"18","type":"AFTER_TEST_CASE","tagExpression":"@fail-after","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":15}}}} +{"hook":{"id":"19","type":"AFTER_TEST_CASE","tagExpression":"@passing-hook","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":19}}}} +{"testRunStarted":{"id":"20","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"21","pickleId":"10","testSteps":[{"id":"22","hookId":"16"},{"id":"23","pickleStepId":"9","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"24","pickleId":"12","testSteps":[{"id":"25","pickleStepId":"11","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"26","hookId":"18"}],"testRunStartedId":"20"}} +{"testCase":{"id":"27","pickleId":"14","testSteps":[{"id":"28","hookId":"15"},{"id":"29","pickleStepId":"13","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"30","hookId":"19"}],"testRunStartedId":"20"}} +{"testCaseStarted":{"id":"31","testCaseId":"21","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"22","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"22","testStepResult":{"message":"Exception in conditional hook","exception":{"type":"Error","message":"Exception in conditional hook","stackTrace":"Error: Exception in conditional hook\nsamples/hooks-conditional/hooks-conditional.feature:7"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"23","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"23","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"seconds":0,"nanos":6000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"32","testCaseId":"24","timestamp":{"seconds":0,"nanos":7000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"25","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"25","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"26","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"26","testStepResult":{"message":"Exception in conditional hook","exception":{"type":"Error","message":"Exception in conditional hook","stackTrace":"Error: Exception in conditional hook\nsamples/hooks-conditional/hooks-conditional.feature:11"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"33","testCaseId":"27","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"28","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"28","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"29","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"29","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"30","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"20","timestamp":{"seconds":0,"nanos":21000000},"success":false}} diff --git a/testdata/src/hooks-named.ndjson b/testdata/src/hooks-named.ndjson new file mode 100644 index 0000000..84fff79 --- /dev/null +++ b/testdata/src/hooks-named.ndjson @@ -0,0 +1,18 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - Named\n Hooks are special steps that run before or after each scenario's steps.\n\n Hooks can be given a name. Which is nice for reporting. Otherwise they work\n exactly the same as regular hooks.\n\n Scenario: With a named before and after hook\n When a step passes\n","uri":"samples/hooks-named/hooks-named.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - Named","description":" Hooks are special steps that run before or after each scenario's steps.\n\n Hooks can be given a name. Which is nice for reporting. Otherwise they work\n exactly the same as regular hooks.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"With a named before and after hook","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-named/hooks-named.feature"}} +{"pickle":{"id":"3","uri":"samples/hooks-named/hooks-named.feature","astNodeIds":["1"],"tags":[],"name":"With a named before and after hook","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks-named/hooks-named.ts","location":{"line":7}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_CASE","name":"A named before hook","sourceReference":{"uri":"samples/hooks-named/hooks-named.ts","location":{"line":3}}}} +{"hook":{"id":"6","type":"AFTER_TEST_CASE","name":"A named after hook","sourceReference":{"uri":"samples/hooks-named/hooks-named.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"7","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"8","pickleId":"3","testSteps":[{"id":"9","hookId":"4"},{"id":"10","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"11","hookId":"6"}],"testRunStartedId":"7"}} +{"testCaseStarted":{"id":"12","testCaseId":"8","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"9","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"9","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"10","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"11","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"11","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"12","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"7","timestamp":{"seconds":0,"nanos":9000000},"success":true}} diff --git a/testdata/src/hooks-undefined.ndjson b/testdata/src/hooks-undefined.ndjson new file mode 100644 index 0000000..4d90c89 --- /dev/null +++ b/testdata/src/hooks-undefined.ndjson @@ -0,0 +1,18 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - With Undefined Steps\n Hooks also run before and after undefined steps\n\n Scenario: No tags and a undefined step\n When a step does not exist\n","uri":"samples/hooks-undefined/hooks-undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - With Undefined Steps","description":" Hooks also run before and after undefined steps","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"No tags and a undefined step","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step does not exist"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-undefined/hooks-undefined.feature"}} +{"pickle":{"id":"3","uri":"samples/hooks-undefined/hooks-undefined.feature","astNodeIds":["1"],"tags":[],"name":"No tags and a undefined step","language":"en","steps":[{"id":"2","text":"a step does not exist","type":"Action","astNodeIds":["0"]}]}} +{"hook":{"id":"4","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks-undefined/hooks-undefined.ts","location":{"line":3}}}} +{"hook":{"id":"5","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks-undefined/hooks-undefined.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","hookId":"4"},{"id":"9","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"10","hookId":"5"}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"11","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"8","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"9","timestamp":{"seconds":0,"nanos":4000000}}} +{"suggestion":{"id":"12","pickleStepId":"2","snippets":[{"language":"typescript","code":"When(\"a step does not exist\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"9","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"10","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"11","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":9000000},"success":false}} diff --git a/testdata/src/hooks.ndjson b/testdata/src/hooks.ndjson new file mode 100644 index 0000000..c99fa75 --- /dev/null +++ b/testdata/src/hooks.ndjson @@ -0,0 +1,29 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks\n Hooks are special steps that run before or after each scenario's steps.\n\n Scenario: No tags and a passed step\n When a step passes\n\n Scenario: No tags and a failed step\n When a step fails\n","uri":"samples/hooks/hooks.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks","description":" Hooks are special steps that run before or after each scenario's steps.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"No tags and a passed step","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"No tags and a failed step","description":"","steps":[{"id":"2","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step fails"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"id":"5","uri":"samples/hooks/hooks.feature","astNodeIds":["1"],"tags":[],"name":"No tags and a passed step","language":"en","steps":[{"id":"4","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"7","uri":"samples/hooks/hooks.feature","astNodeIds":["3"],"tags":[],"name":"No tags and a failed step","language":"en","steps":[{"id":"6","text":"a step fails","type":"Action","astNodeIds":["2"]}]}} +{"stepDefinition":{"id":"9","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step fails"},"sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":11}}}} +{"hook":{"id":"8","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":3}}}} +{"hook":{"id":"11","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":15}}}} +{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"13","pickleId":"5","testSteps":[{"id":"14","hookId":"8"},{"id":"15","pickleStepId":"4","stepDefinitionIds":["9"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"16","hookId":"11"}],"testRunStartedId":"12"}} +{"testCase":{"id":"17","pickleId":"7","testSteps":[{"id":"18","hookId":"8"},{"id":"19","pickleStepId":"6","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"20","hookId":"11"}],"testRunStartedId":"12"}} +{"testCaseStarted":{"id":"21","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"15","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"16","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"16","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"22","testCaseId":"17","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"18","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"18","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"19","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"19","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/hooks/hooks.feature:8"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"20","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"20","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":17000000},"success":false}} diff --git a/testdata/src/markdown.ndjson b/testdata/src/markdown.ndjson new file mode 100644 index 0000000..78a92b1 --- /dev/null +++ b/testdata/src/markdown.ndjson @@ -0,0 +1,35 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"# Feature: Cheese\n\nThis table is not picked up by Gherkin (not indented 2+ spaces)\n\n| foo | bar |\n| --- | --- |\n| boz | boo |\n\n\n## Rule: Nom nom nom\n\nI love cheese, especially fromage macaroni cheese. Rubber cheese ricotta caerphilly blue castello who moved my cheese queso bavarian bergkase melted cheese.\n\n### Scenario Outline: Ylajali!\n\n* Given some TypeScript code:\n ```typescript\n type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'\n ```\n* And some classic Gherkin:\n ```gherkin\n Given there are 24 apples in Mary's basket\n ```\n* When we use a data table and attach something and then \n | name | age |\n | ---- | --: |\n | Bill | 3 |\n | Jane | 6 |\n | Isla | 5 |\n* Then this might or might not run\n\n#### Examples: because we need more tables\n\nThis table is indented 2 spaces, so Gherkin will pick it up\n\n | what |\n | ---- |\n | fail |\n | pass |\n\nAnd oh by the way, this table is also ignored by Gherkin because it doesn't have 2+ space indent:\n\n| cheese |\n| -------- |\n| gouda |\n| gamalost |\n","uri":"samples/markdown/markdown.feature.md","mediaType":"text/x.cucumber.gherkin+markdown"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":3},"language":"en","keyword":"Feature","name":"Cheese","description":"| boz | boo |","children":[{"rule":{"id":"13","location":{"line":10,"column":4},"keyword":"Rule","name":"Nom nom nom","description":"","children":[{"scenario":{"id":"12","tags":[],"location":{"line":14,"column":5},"keyword":"Scenario Outline","name":"Ylajali!","description":"","steps":[{"id":"0","location":{"line":16,"column":3},"keyword":"Given ","keywordType":"Context","text":"some TypeScript code:","docString":{"location":{"line":17,"column":3},"content":"type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'","delimiter":"```","mediaType":"typescript"}},{"id":"1","location":{"line":20,"column":3},"keyword":"And ","keywordType":"Conjunction","text":"some classic Gherkin:","docString":{"location":{"line":21,"column":3},"content":"Given there are 24 apples in Mary's basket","delimiter":"```","mediaType":"gherkin"}},{"id":"6","location":{"line":24,"column":3},"keyword":"When ","keywordType":"Action","text":"we use a data table and attach something and then ","dataTable":{"location":{"line":25,"column":3},"rows":[{"id":"2","location":{"line":25,"column":3},"cells":[{"location":{"line":25,"column":5},"value":"name"},{"location":{"line":25,"column":12},"value":"age"}]},{"id":"3","location":{"line":27,"column":3},"cells":[{"location":{"line":27,"column":5},"value":"Bill"},{"location":{"line":27,"column":14},"value":"3"}]},{"id":"4","location":{"line":28,"column":3},"cells":[{"location":{"line":28,"column":5},"value":"Jane"},{"location":{"line":28,"column":14},"value":"6"}]},{"id":"5","location":{"line":29,"column":3},"cells":[{"location":{"line":29,"column":5},"value":"Isla"},{"location":{"line":29,"column":14},"value":"5"}]}]}},{"id":"7","location":{"line":30,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"this might or might not run"}],"examples":[{"id":"11","tags":[],"location":{"line":32,"column":6},"keyword":"Examples","name":"because we need more tables","description":"","tableHeader":{"id":"8","location":{"line":36,"column":3},"cells":[{"location":{"line":36,"column":5},"value":"what"}]},"tableBody":[{"id":"9","location":{"line":38,"column":3},"cells":[{"location":{"line":38,"column":5},"value":"fail"}]},{"id":"10","location":{"line":39,"column":3},"cells":[{"location":{"line":39,"column":5},"value":"pass"}]}]}]}}],"tags":[]}}]},"comments":[],"uri":"samples/markdown/markdown.feature.md"}} +{"pickle":{"id":"18","uri":"samples/markdown/markdown.feature.md","astNodeIds":["12","9"],"name":"Ylajali!","language":"en","steps":[{"id":"14","text":"some TypeScript code:","type":"Context","argument":{"docString":{"content":"type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'","mediaType":"typescript"}},"astNodeIds":["0","9"]},{"id":"15","text":"some classic Gherkin:","type":"Context","argument":{"docString":{"content":"Given there are 24 apples in Mary's basket","mediaType":"gherkin"}},"astNodeIds":["1","9"]},{"id":"16","text":"we use a data table and attach something and then fail","type":"Action","argument":{"dataTable":{"rows":[{"cells":[{"value":"name"},{"value":"age"}]},{"cells":[{"value":"Bill"},{"value":"3"}]},{"cells":[{"value":"Jane"},{"value":"6"}]},{"cells":[{"value":"Isla"},{"value":"5"}]}]}},"astNodeIds":["6","9"]},{"id":"17","text":"this might or might not run","type":"Outcome","astNodeIds":["7","9"]}],"tags":[]}} +{"pickle":{"id":"23","uri":"samples/markdown/markdown.feature.md","astNodeIds":["12","10"],"name":"Ylajali!","language":"en","steps":[{"id":"19","text":"some TypeScript code:","type":"Context","argument":{"docString":{"content":"type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'","mediaType":"typescript"}},"astNodeIds":["0","10"]},{"id":"20","text":"some classic Gherkin:","type":"Context","argument":{"docString":{"content":"Given there are 24 apples in Mary's basket","mediaType":"gherkin"}},"astNodeIds":["1","10"]},{"id":"21","text":"we use a data table and attach something and then pass","type":"Action","argument":{"dataTable":{"rows":[{"cells":[{"value":"name"},{"value":"age"}]},{"cells":[{"value":"Bill"},{"value":"3"}]},{"cells":[{"value":"Jane"},{"value":"6"}]},{"cells":[{"value":"Isla"},{"value":"5"}]}]}},"astNodeIds":["6","10"]},{"id":"22","text":"this might or might not run","type":"Outcome","astNodeIds":["7","10"]}],"tags":[]}} +{"stepDefinition":{"id":"24","pattern":{"type":"CUCUMBER_EXPRESSION","source":"some TypeScript code:"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"25","pattern":{"type":"CUCUMBER_EXPRESSION","source":"some classic Gherkin:"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"26","pattern":{"type":"CUCUMBER_EXPRESSION","source":"we use a data table and attach something and then {word}"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"27","pattern":{"type":"CUCUMBER_EXPRESSION","source":"this might or might not run"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"28","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"29","pickleId":"18","testSteps":[{"id":"30","pickleStepId":"14","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"15","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"32","pickleStepId":"16","stepDefinitionIds":["26"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":50,"value":"fail","children":[]},"parameterTypeName":"word"}]}]},{"id":"33","pickleStepId":"17","stepDefinitionIds":["27"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"28"}} +{"testCase":{"id":"34","pickleId":"23","testSteps":[{"id":"35","pickleStepId":"19","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"36","pickleStepId":"20","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"37","pickleStepId":"21","stepDefinitionIds":["26"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":50,"value":"pass","children":[]},"parameterTypeName":"word"}]}]},{"id":"38","pickleStepId":"22","stepDefinitionIds":["27"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"28"}} +{"testCaseStarted":{"id":"39","testCaseId":"29","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"30","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"31","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"31","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"32","timestamp":{"seconds":0,"nanos":6000000}}} +{"attachment":{"testCaseStartedId":"39","testStepId":"32","body":"We are logging some plain text (fail)","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"32","testStepResult":{"message":"You asked me to fail","exception":{"type":"Error","message":"You asked me to fail","stackTrace":"Error: You asked me to fail\nsamples/markdown/markdown.feature.md:24"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"33","timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"33","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":10000000}}} +{"testCaseFinished":{"testCaseStartedId":"39","timestamp":{"seconds":0,"nanos":11000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"40","testCaseId":"34","timestamp":{"seconds":0,"nanos":12000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"35","timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"36","timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"37","timestamp":{"seconds":0,"nanos":17000000}}} +{"attachment":{"testCaseStartedId":"40","testStepId":"37","body":"We are logging some plain text (pass)","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"38","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"38","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testCaseFinished":{"testCaseStartedId":"40","timestamp":{"seconds":0,"nanos":22000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"28","timestamp":{"seconds":0,"nanos":23000000},"success":false}} diff --git a/testdata/src/minimal.json b/testdata/src/minimal.json new file mode 100644 index 0000000..3926c21 --- /dev/null +++ b/testdata/src/minimal.json @@ -0,0 +1,20 @@ +{ + "stepDefinitions": [ + { + "expression": "I have {int} cukes in my belly", + "location": "samples/minimal/minimal.ts:3", + "duration": { + "sum": 0.001000000, + "mean": 0.001000000, + "moe95": 0.0 + }, + "steps": [ + { + "text": "I have 42 cukes in my belly", + "duration": 0.001000000, + "location": "samples/minimal/minimal.feature:9" + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/src/minimal.ndjson b/testdata/src/minimal.ndjson new file mode 100644 index 0000000..dc5f170 --- /dev/null +++ b/testdata/src/minimal.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: minimal\n \n Cucumber doesn't execute this markdown, but @cucumber/react renders it.\n \n * This is\n * a bullet\n * list\n \n Scenario: cukes\n Given I have 42 cukes in my belly\n","uri":"samples/minimal/minimal.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"minimal","description":" Cucumber doesn't execute this markdown, but @cucumber/react renders it.\n \n * This is\n * a bullet\n * list","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"cukes","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"I have 42 cukes in my belly"}],"examples":[]}}]},"comments":[],"uri":"samples/minimal/minimal.feature"}} +{"pickle":{"id":"3","uri":"samples/minimal/minimal.feature","astNodeIds":["1"],"tags":[],"name":"cukes","language":"en","steps":[{"id":"2","text":"I have 42 cukes in my belly","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I have {int} cukes in my belly"},"sourceReference":{"uri":"samples/minimal/minimal.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":7,"value":"42","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/testdata/src/minimal.plain.txt b/testdata/src/minimal.plain.txt new file mode 100644 index 0000000..dfcd1b6 --- /dev/null +++ b/testdata/src/minimal.plain.txt @@ -0,0 +1,4 @@ + +Expression/Text Duration Mean ± Error Location +I have {int} cukes in my belly 0.001s 0.001s ± 0.000s samples/minimal/minimal.ts:3 + I have 42 cukes in my belly 0.001s samples/minimal/minimal.feature:9 diff --git a/testdata/src/multiple-features-reversed.ndjson b/testdata/src/multiple-features-reversed.ndjson new file mode 100644 index 0000000..6abdaea --- /dev/null +++ b/testdata/src/multiple-features-reversed.ndjson @@ -0,0 +1,64 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: First feature\n\n Scenario: First scenario\n Given an order for \"eggs\"\n\n Scenario: Second scenario\n Given an order for \"milk\"\n\n Scenario: Third scenario\n Given an order for \"bread\"","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"First feature","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"0","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"2","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"milk\""}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"4","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"bread\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature"}} +{"pickle":{"id":"7","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","astNodeIds":["1"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"6","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"9","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","astNodeIds":["3"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"8","text":"an order for \"milk\"","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"11","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","astNodeIds":["5"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"10","text":"an order for \"bread\"","type":"Context","astNodeIds":["4"]}]}} +{"source":{"data":"Feature: Second feature\n\n Scenario: First scenario\n Given an order for \"batteries\"\n\n Scenario: Second scenario\n Given an order for \"light bulbs\"\n\n Scenario: Third scenario\n Given an order for \"fuses\"","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Second feature","description":"","children":[{"scenario":{"id":"13","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"12","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"batteries\""}],"examples":[]}},{"scenario":{"id":"15","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"14","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"light bulbs\""}],"examples":[]}},{"scenario":{"id":"17","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"16","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"fuses\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature"}} +{"pickle":{"id":"19","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","astNodeIds":["13"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"18","text":"an order for \"batteries\"","type":"Context","astNodeIds":["12"]}]}} +{"pickle":{"id":"21","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","astNodeIds":["15"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"20","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["14"]}]}} +{"pickle":{"id":"23","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","astNodeIds":["17"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"22","text":"an order for \"fuses\"","type":"Context","astNodeIds":["16"]}]}} +{"source":{"data":"Feature: Third feature\n\n Scenario: First scenario\n Given an order for \"pencils\"\n\n Scenario: Second scenario\n Given an order for \"rulers\"\n\n Scenario: Third scenario\n Given an order for \"paperclips\"","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Third feature","description":"","children":[{"scenario":{"id":"25","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"24","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"pencils\""}],"examples":[]}},{"scenario":{"id":"27","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"26","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"rulers\""}],"examples":[]}},{"scenario":{"id":"29","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"28","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"paperclips\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature"}} +{"pickle":{"id":"31","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","astNodeIds":["25"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"30","text":"an order for \"pencils\"","type":"Context","astNodeIds":["24"]}]}} +{"pickle":{"id":"33","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","astNodeIds":["27"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"32","text":"an order for \"rulers\"","type":"Context","astNodeIds":["26"]}]}} +{"pickle":{"id":"35","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","astNodeIds":["29"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"34","text":"an order for \"paperclips\"","type":"Context","astNodeIds":["28"]}]}} +{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/multiple-features-reversed/multiple-features-reversed.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"37","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"38","pickleId":"35","testSteps":[{"id":"39","pickleStepId":"34","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"paperclips\"","children":[{"start":14,"value":"paperclips","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"40","pickleId":"33","testSteps":[{"id":"41","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"rulers\"","children":[{"start":14,"value":"rulers","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"42","pickleId":"31","testSteps":[{"id":"43","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"pencils\"","children":[{"start":14,"value":"pencils","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"44","pickleId":"23","testSteps":[{"id":"45","pickleStepId":"22","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"fuses\"","children":[{"start":14,"value":"fuses","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"46","pickleId":"21","testSteps":[{"id":"47","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"48","pickleId":"19","testSteps":[{"id":"49","pickleStepId":"18","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"50","pickleId":"11","testSteps":[{"id":"51","pickleStepId":"10","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"52","pickleId":"9","testSteps":[{"id":"53","pickleStepId":"8","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"54","pickleId":"7","testSteps":[{"id":"55","pickleStepId":"6","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCaseStarted":{"id":"56","testCaseId":"38","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"39","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"57","testCaseId":"40","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"41","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"58","testCaseId":"42","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"43","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"59","testCaseId":"44","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"60","testCaseId":"46","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"47","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"61","testCaseId":"48","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"49","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"61","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"62","testCaseId":"50","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"62","testStepId":"51","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"62","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testCaseFinished":{"testCaseStartedId":"62","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"63","testCaseId":"52","timestamp":{"seconds":0,"nanos":29000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"63","testStepId":"53","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"63","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"63","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"64","testCaseId":"54","timestamp":{"seconds":0,"nanos":33000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"64","testStepId":"55","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"64","testStepId":"55","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testCaseFinished":{"testCaseStartedId":"64","timestamp":{"seconds":0,"nanos":36000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"37","timestamp":{"seconds":0,"nanos":37000000},"success":true}} diff --git a/testdata/src/multiple-features.json b/testdata/src/multiple-features.json new file mode 100644 index 0000000..3b6d56a --- /dev/null +++ b/testdata/src/multiple-features.json @@ -0,0 +1,60 @@ +{ + "stepDefinitions": [ + { + "expression": "an order for {string}", + "location": "samples/multiple-features/multiple-features.ts:3", + "duration": { + "sum": 0.009000000, + "mean": 0.001000000, + "moe95": 0.0 + }, + "steps": [ + { + "text": "an order for \"eggs\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-1.feature:3" + }, + { + "text": "an order for \"milk\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-1.feature:6" + }, + { + "text": "an order for \"bread\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-1.feature:9" + }, + { + "text": "an order for \"batteries\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-2.feature:3" + }, + { + "text": "an order for \"light bulbs\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-2.feature:6" + }, + { + "text": "an order for \"fuses\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-2.feature:9" + }, + { + "text": "an order for \"pencils\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-3.feature:3" + }, + { + "text": "an order for \"rulers\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-3.feature:6" + }, + { + "text": "an order for \"paperclips\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-3.feature:9" + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/src/multiple-features.ndjson b/testdata/src/multiple-features.ndjson new file mode 100644 index 0000000..065a17c --- /dev/null +++ b/testdata/src/multiple-features.ndjson @@ -0,0 +1,64 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: First feature\n\n Scenario: First scenario\n Given an order for \"eggs\"\n\n Scenario: Second scenario\n Given an order for \"milk\"\n\n Scenario: Third scenario\n Given an order for \"bread\"","uri":"samples/multiple-features/multiple-features-1.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"First feature","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"0","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"2","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"milk\""}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"4","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"bread\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features/multiple-features-1.feature"}} +{"pickle":{"id":"7","uri":"samples/multiple-features/multiple-features-1.feature","astNodeIds":["1"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"6","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"9","uri":"samples/multiple-features/multiple-features-1.feature","astNodeIds":["3"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"8","text":"an order for \"milk\"","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"11","uri":"samples/multiple-features/multiple-features-1.feature","astNodeIds":["5"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"10","text":"an order for \"bread\"","type":"Context","astNodeIds":["4"]}]}} +{"source":{"data":"Feature: Second feature\n\n Scenario: First scenario\n Given an order for \"batteries\"\n\n Scenario: Second scenario\n Given an order for \"light bulbs\"\n\n Scenario: Third scenario\n Given an order for \"fuses\"","uri":"samples/multiple-features/multiple-features-2.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Second feature","description":"","children":[{"scenario":{"id":"13","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"12","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"batteries\""}],"examples":[]}},{"scenario":{"id":"15","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"14","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"light bulbs\""}],"examples":[]}},{"scenario":{"id":"17","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"16","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"fuses\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features/multiple-features-2.feature"}} +{"pickle":{"id":"19","uri":"samples/multiple-features/multiple-features-2.feature","astNodeIds":["13"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"18","text":"an order for \"batteries\"","type":"Context","astNodeIds":["12"]}]}} +{"pickle":{"id":"21","uri":"samples/multiple-features/multiple-features-2.feature","astNodeIds":["15"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"20","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["14"]}]}} +{"pickle":{"id":"23","uri":"samples/multiple-features/multiple-features-2.feature","astNodeIds":["17"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"22","text":"an order for \"fuses\"","type":"Context","astNodeIds":["16"]}]}} +{"source":{"data":"Feature: Third feature\n\n Scenario: First scenario\n Given an order for \"pencils\"\n\n Scenario: Second scenario\n Given an order for \"rulers\"\n\n Scenario: Third scenario\n Given an order for \"paperclips\"","uri":"samples/multiple-features/multiple-features-3.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Third feature","description":"","children":[{"scenario":{"id":"25","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"24","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"pencils\""}],"examples":[]}},{"scenario":{"id":"27","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"26","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"rulers\""}],"examples":[]}},{"scenario":{"id":"29","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"28","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"paperclips\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features/multiple-features-3.feature"}} +{"pickle":{"id":"31","uri":"samples/multiple-features/multiple-features-3.feature","astNodeIds":["25"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"30","text":"an order for \"pencils\"","type":"Context","astNodeIds":["24"]}]}} +{"pickle":{"id":"33","uri":"samples/multiple-features/multiple-features-3.feature","astNodeIds":["27"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"32","text":"an order for \"rulers\"","type":"Context","astNodeIds":["26"]}]}} +{"pickle":{"id":"35","uri":"samples/multiple-features/multiple-features-3.feature","astNodeIds":["29"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"34","text":"an order for \"paperclips\"","type":"Context","astNodeIds":["28"]}]}} +{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/multiple-features/multiple-features.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"37","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"38","pickleId":"7","testSteps":[{"id":"39","pickleStepId":"6","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"40","pickleId":"9","testSteps":[{"id":"41","pickleStepId":"8","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"42","pickleId":"11","testSteps":[{"id":"43","pickleStepId":"10","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"44","pickleId":"19","testSteps":[{"id":"45","pickleStepId":"18","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"46","pickleId":"21","testSteps":[{"id":"47","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"48","pickleId":"23","testSteps":[{"id":"49","pickleStepId":"22","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"fuses\"","children":[{"start":14,"value":"fuses","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"50","pickleId":"31","testSteps":[{"id":"51","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"pencils\"","children":[{"start":14,"value":"pencils","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"52","pickleId":"33","testSteps":[{"id":"53","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"rulers\"","children":[{"start":14,"value":"rulers","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"54","pickleId":"35","testSteps":[{"id":"55","pickleStepId":"34","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"paperclips\"","children":[{"start":14,"value":"paperclips","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCaseStarted":{"id":"56","testCaseId":"38","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"39","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"57","testCaseId":"40","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"41","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"58","testCaseId":"42","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"43","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"59","testCaseId":"44","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"60","testCaseId":"46","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"47","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"61","testCaseId":"48","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"49","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"61","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"62","testCaseId":"50","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"62","testStepId":"51","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"62","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testCaseFinished":{"testCaseStartedId":"62","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"63","testCaseId":"52","timestamp":{"seconds":0,"nanos":29000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"63","testStepId":"53","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"63","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"63","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"64","testCaseId":"54","timestamp":{"seconds":0,"nanos":33000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"64","testStepId":"55","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"64","testStepId":"55","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testCaseFinished":{"testCaseStartedId":"64","timestamp":{"seconds":0,"nanos":36000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"37","timestamp":{"seconds":0,"nanos":37000000},"success":true}} diff --git a/testdata/src/multiple-features.plain.txt b/testdata/src/multiple-features.plain.txt new file mode 100644 index 0000000..8a08d8b --- /dev/null +++ b/testdata/src/multiple-features.plain.txt @@ -0,0 +1,9 @@ + +Expression/Text Duration Mean ± Error Location +an order for {string} 0.009s 0.001s ± 0.000s samples/multiple-features/multiple-features.ts:3 + an order for "eggs" 0.001s samples/multiple-features/multiple-features-1.feature:3 + an order for "milk" 0.001s samples/multiple-features/multiple-features-1.feature:6 + an order for "bread" 0.001s samples/multiple-features/multiple-features-1.feature:9 + an order for "batteries" 0.001s samples/multiple-features/multiple-features-2.feature:3 + an order for "light bulbs" 0.001s samples/multiple-features/multiple-features-2.feature:6 + 4 more diff --git a/testdata/src/parameter-types.ndjson b/testdata/src/parameter-types.ndjson new file mode 100644 index 0000000..0a7b63c --- /dev/null +++ b/testdata/src/parameter-types.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.\n\n Scenario: Flight transformer\n Given LHR-CDG has been delayed\n","uri":"samples/parameter-types/parameter-types.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Parameter Types","description":" Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":10,"column":3},"keyword":"Scenario","name":"Flight transformer","description":"","steps":[{"id":"0","location":{"line":11,"column":5},"keyword":"Given ","keywordType":"Context","text":"LHR-CDG has been delayed"}],"examples":[]}}]},"comments":[],"uri":"samples/parameter-types/parameter-types.feature"}} +{"pickle":{"id":"3","uri":"samples/parameter-types/parameter-types.feature","astNodeIds":["1"],"tags":[],"name":"Flight transformer","language":"en","steps":[{"id":"2","text":"LHR-CDG has been delayed","type":"Context","astNodeIds":["0"]}]}} +{"parameterType":{"id":"4","name":"flight","regularExpressions":["([A-Z]{3})-([A-Z]{3})"],"preferForRegularExpressionMatch":false,"useForSnippets":true,"sourceReference":{"uri":"samples/parameter-types/parameter-types.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"{flight} has been delayed"},"sourceReference":{"uri":"samples/parameter-types/parameter-types.ts","location":{"line":16}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":0,"value":"LHR-CDG","children":[{"start":0,"value":"LHR","children":[]},{"start":4,"value":"CDG","children":[]}]},"parameterTypeName":"flight"}]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/testdata/src/pending.ndjson b/testdata/src/pending.ndjson new file mode 100644 index 0000000..259b5e1 --- /dev/null +++ b/testdata/src/pending.ndjson @@ -0,0 +1,30 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Pending steps\n During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.\n\n Scenario: Unimplemented step signals pending status\n Given an unimplemented pending step\n\n Scenario: Steps before unimplemented steps are executed\n Given an implemented non-pending step\n And an unimplemented pending step\n\n Scenario: Steps after unimplemented steps are skipped\n Given an unimplemented pending step\n And an implemented step that is skipped\n","uri":"samples/pending/pending.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Pending steps","description":" During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Unimplemented step signals pending status","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an unimplemented pending step"}],"examples":[]}},{"scenario":{"id":"4","tags":[],"location":{"line":12,"column":3},"keyword":"Scenario","name":"Steps before unimplemented steps are executed","description":"","steps":[{"id":"2","location":{"line":13,"column":5},"keyword":"Given ","keywordType":"Context","text":"an implemented non-pending step"},{"id":"3","location":{"line":14,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an unimplemented pending step"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":16,"column":3},"keyword":"Scenario","name":"Steps after unimplemented steps are skipped","description":"","steps":[{"id":"5","location":{"line":17,"column":5},"keyword":"Given ","keywordType":"Context","text":"an unimplemented pending step"},{"id":"6","location":{"line":18,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an implemented step that is skipped"}],"examples":[]}}]},"comments":[],"uri":"samples/pending/pending.feature"}} +{"pickle":{"id":"9","uri":"samples/pending/pending.feature","astNodeIds":["1"],"tags":[],"name":"Unimplemented step signals pending status","language":"en","steps":[{"id":"8","text":"an unimplemented pending step","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"12","uri":"samples/pending/pending.feature","astNodeIds":["4"],"tags":[],"name":"Steps before unimplemented steps are executed","language":"en","steps":[{"id":"10","text":"an implemented non-pending step","type":"Context","astNodeIds":["2"]},{"id":"11","text":"an unimplemented pending step","type":"Context","astNodeIds":["3"]}]}} +{"pickle":{"id":"15","uri":"samples/pending/pending.feature","astNodeIds":["7"],"tags":[],"name":"Steps after unimplemented steps are skipped","language":"en","steps":[{"id":"13","text":"an unimplemented pending step","type":"Context","astNodeIds":["5"]},{"id":"14","text":"an implemented step that is skipped","type":"Context","astNodeIds":["6"]}]}} +{"stepDefinition":{"id":"16","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented non-pending step"},"sourceReference":{"uri":"samples/pending/pending.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented step that is skipped"},"sourceReference":{"uri":"samples/pending/pending.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"18","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an unimplemented pending step"},"sourceReference":{"uri":"samples/pending/pending.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"19","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"20","pickleId":"9","testSteps":[{"id":"21","pickleStepId":"8","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"19"}} +{"testCase":{"id":"22","pickleId":"12","testSteps":[{"id":"23","pickleStepId":"10","stepDefinitionIds":["16"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"24","pickleStepId":"11","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"19"}} +{"testCase":{"id":"25","pickleId":"15","testSteps":[{"id":"26","pickleStepId":"13","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"27","pickleStepId":"14","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"19"}} +{"testCaseStarted":{"id":"28","testCaseId":"20","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"21","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"21","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"29","testCaseId":"22","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"23","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"23","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"24","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"24","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"30","testCaseId":"25","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"26","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"26","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"27","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"27","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"19","timestamp":{"seconds":0,"nanos":17000000},"success":false}} diff --git a/testdata/src/regular-expression.ndjson b/testdata/src/regular-expression.ndjson new file mode 100644 index 0000000..de09405 --- /dev/null +++ b/testdata/src/regular-expression.ndjson @@ -0,0 +1,16 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: regular expression\n \n Cucumber supports both Cucumber and regular expressions in step\n definitions. This shows a sample that uses a regular expression.\n \n Scenario: regular expression\n Given a cucumber\n Given a cucumber and a zucchini\n Given a cucumber and a zucchini and a gourd\n","uri":"samples/regular-expression/regular-expression.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"regular expression","description":" Cucumber supports both Cucumber and regular expressions in step\n definitions. This shows a sample that uses a regular expression.","children":[{"scenario":{"id":"3","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"regular expression","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"a cucumber"},{"id":"1","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"a cucumber and a zucchini"},{"id":"2","location":{"line":9,"column":5},"keyword":"Given ","keywordType":"Context","text":"a cucumber and a zucchini and a gourd"}],"examples":[]}}]},"comments":[],"uri":"samples/regular-expression/regular-expression.feature"}} +{"pickle":{"id":"7","uri":"samples/regular-expression/regular-expression.feature","astNodeIds":["3"],"tags":[],"name":"regular expression","language":"en","steps":[{"id":"4","text":"a cucumber","type":"Context","astNodeIds":["0"]},{"id":"5","text":"a cucumber and a zucchini","type":"Context","astNodeIds":["1"]},{"id":"6","text":"a cucumber and a zucchini and a gourd","type":"Context","astNodeIds":["2"]}]}} +{"stepDefinition":{"id":"8","pattern":{"type":"REGULAR_EXPRESSION","source":"^a (.*?)(?: and a (.*?))?(?: and a (.*?))?$"},"sourceReference":{"uri":"samples/regular-expression/regular-expression.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"9","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"10","pickleId":"7","testSteps":[{"id":"11","pickleStepId":"4","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"cucumber","children":[]}},{"group":{"children":[]}},{"group":{"children":[]}}]}]},{"id":"12","pickleStepId":"5","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"cucumber","children":[]}},{"group":{"start":17,"value":"zucchini","children":[]}},{"group":{"children":[]}}]}]},{"id":"13","pickleStepId":"6","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"cucumber","children":[]}},{"group":{"start":17,"value":"zucchini","children":[]}},{"group":{"start":32,"value":"gourd","children":[]}}]}]}],"testRunStartedId":"9"}} +{"testCaseStarted":{"id":"14","testCaseId":"10","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"14","testStepId":"11","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"14","testStepId":"11","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"14","testStepId":"12","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"14","testStepId":"12","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"14","testStepId":"13","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"14","testStepId":"13","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"14","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"9","timestamp":{"seconds":0,"nanos":9000000},"success":true}} diff --git a/testdata/src/retry-ambiguous.ndjson b/testdata/src/retry-ambiguous.ndjson new file mode 100644 index 0000000..5d3e0d1 --- /dev/null +++ b/testdata/src/retry-ambiguous.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry - With Ambiguous Steps\n Scenario: Test cases won't retry when the status is AMBIGUOUS\n Given an ambiguous step\n","uri":"samples/retry-ambiguous/retry-ambiguous.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry - With Ambiguous Steps","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"Test cases won't retry when the status is AMBIGUOUS","description":"","steps":[{"id":"0","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"an ambiguous step"}],"examples":[]}}]},"comments":[],"uri":"samples/retry-ambiguous/retry-ambiguous.feature"}} +{"pickle":{"id":"3","uri":"samples/retry-ambiguous/retry-ambiguous.feature","astNodeIds":["1"],"tags":[],"name":"Test cases won't retry when the status is AMBIGUOUS","language":"en","steps":[{"id":"2","text":"an ambiguous step","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an ambiguous step"},"sourceReference":{"uri":"samples/retry-ambiguous/retry-ambiguous.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an ambiguous step"},"sourceReference":{"uri":"samples/retry-ambiguous/retry-ambiguous.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["4","5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]},{"stepMatchArguments":[]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"AMBIGUOUS","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/testdata/src/retry-pending.ndjson b/testdata/src/retry-pending.ndjson new file mode 100644 index 0000000..21f00d7 --- /dev/null +++ b/testdata/src/retry-pending.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry - With Pending Steps\n Scenario: Test cases won't retry when the status is PENDING\n Given a pending step\n","uri":"samples/retry-pending/retry-pending.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry - With Pending Steps","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"Test cases won't retry when the status is PENDING","description":"","steps":[{"id":"0","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a pending step"}],"examples":[]}}]},"comments":[],"uri":"samples/retry-pending/retry-pending.feature"}} +{"pickle":{"id":"3","uri":"samples/retry-pending/retry-pending.feature","astNodeIds":["1"],"tags":[],"name":"Test cases won't retry when the status is PENDING","language":"en","steps":[{"id":"2","text":"a pending step","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a pending step"},"sourceReference":{"uri":"samples/retry-pending/retry-pending.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/testdata/src/retry-undefined.ndjson b/testdata/src/retry-undefined.ndjson new file mode 100644 index 0000000..1c0e31d --- /dev/null +++ b/testdata/src/retry-undefined.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry - With Undefined Steps\n Scenario: Test cases won't retry when the status is UNDEFINED\n Given a non-existent step\n","uri":"samples/retry-undefined/retry-undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry - With Undefined Steps","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"Test cases won't retry when the status is UNDEFINED","description":"","steps":[{"id":"0","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a non-existent step"}],"examples":[]}}]},"comments":[],"uri":"samples/retry-undefined/retry-undefined.feature"}} +{"pickle":{"id":"3","uri":"samples/retry-undefined/retry-undefined.feature","astNodeIds":["1"],"tags":[],"name":"Test cases won't retry when the status is UNDEFINED","language":"en","steps":[{"id":"2","text":"a non-existent step","type":"Context","astNodeIds":["0"]}]}} +{"testRunStarted":{"id":"4","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"5","pickleId":"3","testSteps":[{"id":"6","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"4"}} +{"testCaseStarted":{"id":"7","testCaseId":"5","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"6","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"8","pickleStepId":"2","snippets":[{"language":"typescript","code":"Given(\"a non-existent step\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"6","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"4","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/testdata/src/retry.ndjson b/testdata/src/retry.ndjson new file mode 100644 index 0000000..1bbc345 --- /dev/null +++ b/testdata/src/retry.ndjson @@ -0,0 +1,53 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry\n Some Cucumber implementations support a Retry mechanism, where test cases that fail\n can be retried up to a limited number of attempts in the same test run.\n\n Non-passing statuses other than FAILED won't trigger a retry, as they are not\n going to pass however many times we attempt them.\n\n Scenario: Test cases that pass aren't retried\n Given a step that always passes\n\n Scenario: Test cases that fail are retried if within the --retry limit\n Given a step that passes the second time\n\n Scenario: Test cases that fail will continue to retry up to the --retry limit\n Given a step that passes the third time\n\n Scenario: Test cases won't retry after failing more than the --retry limit\n Given a step that always fails\n","uri":"samples/retry/retry.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry","description":" Some Cucumber implementations support a Retry mechanism, where test cases that fail\n can be retried up to a limited number of attempts in the same test run.\n\n Non-passing statuses other than FAILED won't trigger a retry, as they are not\n going to pass however many times we attempt them.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":8,"column":3},"keyword":"Scenario","name":"Test cases that pass aren't retried","description":"","steps":[{"id":"0","location":{"line":9,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that always passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Test cases that fail are retried if within the --retry limit","description":"","steps":[{"id":"2","location":{"line":12,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that passes the second time"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":14,"column":3},"keyword":"Scenario","name":"Test cases that fail will continue to retry up to the --retry limit","description":"","steps":[{"id":"4","location":{"line":15,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that passes the third time"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":17,"column":3},"keyword":"Scenario","name":"Test cases won't retry after failing more than the --retry limit","description":"","steps":[{"id":"6","location":{"line":18,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that always fails"}],"examples":[]}}]},"comments":[],"uri":"samples/retry/retry.feature"}} +{"pickle":{"id":"9","uri":"samples/retry/retry.feature","astNodeIds":["1"],"tags":[],"name":"Test cases that pass aren't retried","language":"en","steps":[{"id":"8","text":"a step that always passes","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"11","uri":"samples/retry/retry.feature","astNodeIds":["3"],"tags":[],"name":"Test cases that fail are retried if within the --retry limit","language":"en","steps":[{"id":"10","text":"a step that passes the second time","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"13","uri":"samples/retry/retry.feature","astNodeIds":["5"],"tags":[],"name":"Test cases that fail will continue to retry up to the --retry limit","language":"en","steps":[{"id":"12","text":"a step that passes the third time","type":"Context","astNodeIds":["4"]}]}} +{"pickle":{"id":"15","uri":"samples/retry/retry.feature","astNodeIds":["7"],"tags":[],"name":"Test cases won't retry after failing more than the --retry limit","language":"en","steps":[{"id":"14","text":"a step that always fails","type":"Context","astNodeIds":["6"]}]}} +{"stepDefinition":{"id":"16","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that always passes"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that passes the second time"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"18","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that passes the third time"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":16}}}} +{"stepDefinition":{"id":"19","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that always fails"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"20","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"21","pickleId":"9","testSteps":[{"id":"22","pickleStepId":"8","stepDefinitionIds":["16"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"23","pickleId":"11","testSteps":[{"id":"24","pickleStepId":"10","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"25","pickleId":"13","testSteps":[{"id":"26","pickleStepId":"12","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"27","pickleId":"15","testSteps":[{"id":"28","pickleStepId":"14","stepDefinitionIds":["19"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCaseStarted":{"id":"29","testCaseId":"21","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"22","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"22","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"30","testCaseId":"23","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"24","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"24","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:12"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"31","testCaseId":"23","timestamp":{"seconds":0,"nanos":9000000},"attempt":1}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"24","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"24","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"32","testCaseId":"25","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"26","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"26","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:15"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"33","testCaseId":"25","timestamp":{"seconds":0,"nanos":17000000},"attempt":1}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"26","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"26","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:15"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"34","testCaseId":"25","timestamp":{"seconds":0,"nanos":21000000},"attempt":2}} +{"testStepStarted":{"testCaseStartedId":"34","testStepId":"26","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"34","testStepId":"26","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"34","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"35","testCaseId":"27","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"35","testStepId":"28","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"35","testStepId":"28","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:18"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testCaseFinished":{"testCaseStartedId":"35","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"36","testCaseId":"27","timestamp":{"seconds":0,"nanos":29000000},"attempt":1}} +{"testStepStarted":{"testCaseStartedId":"36","testStepId":"28","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"36","testStepId":"28","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:18"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"36","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"37","testCaseId":"27","timestamp":{"seconds":0,"nanos":33000000},"attempt":2}} +{"testStepStarted":{"testCaseStartedId":"37","testStepId":"28","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"37","testStepId":"28","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:18"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testCaseFinished":{"testCaseStartedId":"37","timestamp":{"seconds":0,"nanos":36000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"20","timestamp":{"seconds":0,"nanos":37000000},"success":false}} diff --git a/testdata/src/rules-backgrounds.ndjson b/testdata/src/rules-backgrounds.ndjson new file mode 100644 index 0000000..96eaa69 --- /dev/null +++ b/testdata/src/rules-backgrounds.ndjson @@ -0,0 +1,44 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Rules with Backgrounds\n Like Features, Rules can also have Backgrounds, whose steps are prepended to those of each child Scenario. Only\n one Background at the Rule level is supported.\n\n It's even possible to have a Background at both Feature and Rule level, in which case they are concatenated.\n\n Background:\n Given an order for \"eggs\"\n And an order for \"milk\"\n And an order for \"bread\"\n\n Rule:\n Background:\n Given an order for \"batteries\"\n And an order for \"light bulbs\"\n\n Example: one scenario\n When an action\n Then an outcome\n\n Example: another scenario\n When an action\n Then an outcome","uri":"samples/rules-backgrounds/rules-backgrounds.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Rules with Backgrounds","description":" Like Features, Rules can also have Backgrounds, whose steps are prepended to those of each child Scenario. Only\n one Background at the Rule level is supported.\n\n It's even possible to have a Background at both Feature and Rule level, in which case they are concatenated.","children":[{"background":{"id":"3","location":{"line":7,"column":3},"keyword":"Background","name":"","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""},{"id":"1","location":{"line":9,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"milk\""},{"id":"2","location":{"line":10,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"bread\""}]}},{"rule":{"id":"13","location":{"line":12,"column":3},"keyword":"Rule","name":"","description":"","children":[{"background":{"id":"6","location":{"line":13,"column":5},"keyword":"Background","name":"","description":"","steps":[{"id":"4","location":{"line":14,"column":7},"keyword":"Given ","keywordType":"Context","text":"an order for \"batteries\""},{"id":"5","location":{"line":15,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"light bulbs\""}]}},{"scenario":{"id":"9","tags":[],"location":{"line":17,"column":5},"keyword":"Example","name":"one scenario","description":"","steps":[{"id":"7","location":{"line":18,"column":7},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"8","location":{"line":19,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}},{"scenario":{"id":"12","tags":[],"location":{"line":21,"column":5},"keyword":"Example","name":"another scenario","description":"","steps":[{"id":"10","location":{"line":22,"column":7},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"11","location":{"line":23,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}}],"tags":[]}}]},"comments":[],"uri":"samples/rules-backgrounds/rules-backgrounds.feature"}} +{"pickle":{"id":"21","uri":"samples/rules-backgrounds/rules-backgrounds.feature","astNodeIds":["9"],"tags":[],"name":"one scenario","language":"en","steps":[{"id":"14","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"15","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"16","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"17","text":"an order for \"batteries\"","type":"Context","astNodeIds":["4"]},{"id":"18","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["5"]},{"id":"19","text":"an action","type":"Action","astNodeIds":["7"]},{"id":"20","text":"an outcome","type":"Outcome","astNodeIds":["8"]}]}} +{"pickle":{"id":"29","uri":"samples/rules-backgrounds/rules-backgrounds.feature","astNodeIds":["12"],"tags":[],"name":"another scenario","language":"en","steps":[{"id":"22","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"23","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"24","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"25","text":"an order for \"batteries\"","type":"Context","astNodeIds":["4"]},{"id":"26","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["5"]},{"id":"27","text":"an action","type":"Action","astNodeIds":["10"]},{"id":"28","text":"an outcome","type":"Outcome","astNodeIds":["11"]}]}} +{"stepDefinition":{"id":"30","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/rules-backgrounds/rules-backgrounds.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"31","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an action"},"sourceReference":{"uri":"samples/rules-backgrounds/rules-backgrounds.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"32","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an outcome"},"sourceReference":{"uri":"samples/rules-backgrounds/rules-backgrounds.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"33","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"34","pickleId":"21","testSteps":[{"id":"35","pickleStepId":"14","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"15","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"37","pickleStepId":"16","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"38","pickleStepId":"17","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"39","pickleStepId":"18","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"40","pickleStepId":"19","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"41","pickleStepId":"20","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"33"}} +{"testCase":{"id":"42","pickleId":"29","testSteps":[{"id":"43","pickleStepId":"22","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"44","pickleStepId":"23","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"45","pickleStepId":"24","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"46","pickleStepId":"25","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"47","pickleStepId":"26","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"48","pickleStepId":"27","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"49","pickleStepId":"28","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"33"}} +{"testCaseStarted":{"id":"50","testCaseId":"34","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"35","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"36","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"37","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"38","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"38","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"39","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"40","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"40","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"41","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"50","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"51","testCaseId":"42","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"43","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"44","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"44","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"45","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"46","timestamp":{"seconds":0,"nanos":24000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"46","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":25000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"47","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"48","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"48","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"49","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"51","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"33","timestamp":{"seconds":0,"nanos":33000000},"success":true}} diff --git a/testdata/src/rules.ndjson b/testdata/src/rules.ndjson new file mode 100644 index 0000000..6431aba --- /dev/null +++ b/testdata/src/rules.ndjson @@ -0,0 +1,47 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Usage of a `Rule`\n You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.\n\n Rule: A sale cannot happen if the customer does not have enough money\n # Unhappy path\n Example: Not enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 125 cent chocolate bar\n Then the sale should not happen\n\n # Happy path\n Example: Enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 75 cent chocolate bar\n Then the sale should happen\n\n @some-tag\n Rule: a sale cannot happen if there is no stock\n # Unhappy path\n Example: No chocolates left\n Given the customer has 100 cents\n And there are no chocolate bars in stock\n When the customer tries to buy a 1 cent chocolate bar\n Then the sale should not happen\n","uri":"samples/rules/rules.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Usage of a `Rule`","description":" You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.","children":[{"rule":{"id":"10","location":{"line":7,"column":3},"keyword":"Rule","name":"A sale cannot happen if the customer does not have enough money","description":"","children":[{"scenario":{"id":"4","tags":[],"location":{"line":9,"column":5},"keyword":"Example","name":"Not enough money","description":"","steps":[{"id":"0","location":{"line":10,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"1","location":{"line":11,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are chocolate bars in stock"},{"id":"2","location":{"line":12,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 125 cent chocolate bar"},{"id":"3","location":{"line":13,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should not happen"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":16,"column":5},"keyword":"Example","name":"Enough money","description":"","steps":[{"id":"5","location":{"line":17,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"6","location":{"line":18,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are chocolate bars in stock"},{"id":"7","location":{"line":19,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 75 cent chocolate bar"},{"id":"8","location":{"line":20,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should happen"}],"examples":[]}}],"tags":[]}},{"rule":{"id":"17","location":{"line":23,"column":3},"keyword":"Rule","name":"a sale cannot happen if there is no stock","description":"","children":[{"scenario":{"id":"15","tags":[],"location":{"line":25,"column":5},"keyword":"Example","name":"No chocolates left","description":"","steps":[{"id":"11","location":{"line":26,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"12","location":{"line":27,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are no chocolate bars in stock"},{"id":"13","location":{"line":28,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 1 cent chocolate bar"},{"id":"14","location":{"line":29,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should not happen"}],"examples":[]}}],"tags":[{"location":{"line":22,"column":3},"name":"@some-tag","id":"16"}]}}]},"comments":[{"location":{"line":8,"column":1},"text":" # Unhappy path"},{"location":{"line":15,"column":1},"text":" # Happy path"},{"location":{"line":24,"column":1},"text":" # Unhappy path"}],"uri":"samples/rules/rules.feature"}} +{"pickle":{"id":"22","uri":"samples/rules/rules.feature","astNodeIds":["4"],"tags":[],"name":"Not enough money","language":"en","steps":[{"id":"18","text":"the customer has 100 cents","type":"Context","astNodeIds":["0"]},{"id":"19","text":"there are chocolate bars in stock","type":"Context","astNodeIds":["1"]},{"id":"20","text":"the customer tries to buy a 125 cent chocolate bar","type":"Action","astNodeIds":["2"]},{"id":"21","text":"the sale should not happen","type":"Outcome","astNodeIds":["3"]}]}} +{"pickle":{"id":"27","uri":"samples/rules/rules.feature","astNodeIds":["9"],"tags":[],"name":"Enough money","language":"en","steps":[{"id":"23","text":"the customer has 100 cents","type":"Context","astNodeIds":["5"]},{"id":"24","text":"there are chocolate bars in stock","type":"Context","astNodeIds":["6"]},{"id":"25","text":"the customer tries to buy a 75 cent chocolate bar","type":"Action","astNodeIds":["7"]},{"id":"26","text":"the sale should happen","type":"Outcome","astNodeIds":["8"]}]}} +{"pickle":{"id":"32","uri":"samples/rules/rules.feature","astNodeIds":["15"],"tags":[{"name":"@some-tag","astNodeId":"16"}],"name":"No chocolates left","language":"en","steps":[{"id":"28","text":"the customer has 100 cents","type":"Context","astNodeIds":["11"]},{"id":"29","text":"there are no chocolate bars in stock","type":"Context","astNodeIds":["12"]},{"id":"30","text":"the customer tries to buy a 1 cent chocolate bar","type":"Action","astNodeIds":["13"]},{"id":"31","text":"the sale should not happen","type":"Outcome","astNodeIds":["14"]}]}} +{"stepDefinition":{"id":"33","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the customer has {int} cents"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"34","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are chocolate bars in stock"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"35","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are no chocolate bars in stock"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the customer tries to buy a {int} cent chocolate bar"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":16}}}} +{"stepDefinition":{"id":"37","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the sale should not happen"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":22}}}} +{"stepDefinition":{"id":"38","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the sale should happen"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":26}}}} +{"testRunStarted":{"id":"39","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"40","pickleId":"22","testSteps":[{"id":"41","pickleStepId":"18","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"42","pickleStepId":"19","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"43","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"125","children":[]},"parameterTypeName":"int"}]}]},{"id":"44","pickleStepId":"21","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}} +{"testCase":{"id":"45","pickleId":"27","testSteps":[{"id":"46","pickleStepId":"23","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"47","pickleStepId":"24","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"48","pickleStepId":"25","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"75","children":[]},"parameterTypeName":"int"}]}]},{"id":"49","pickleStepId":"26","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}} +{"testCase":{"id":"50","pickleId":"32","testSteps":[{"id":"51","pickleStepId":"28","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"29","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"53","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"54","pickleStepId":"31","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}} +{"testCaseStarted":{"id":"55","testCaseId":"40","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"41","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"42","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"42","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"43","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"44","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"44","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"56","testCaseId":"45","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"46","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"46","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"47","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"48","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"48","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"57","testCaseId":"50","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"51","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"52","timestamp":{"seconds":0,"nanos":24000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"52","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":25000000}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"53","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"54","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"54","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":30000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"39","timestamp":{"seconds":0,"nanos":31000000},"success":true}} diff --git a/testdata/src/skipped.ndjson b/testdata/src/skipped.ndjson new file mode 100644 index 0000000..a6832c2 --- /dev/null +++ b/testdata/src/skipped.ndjson @@ -0,0 +1,33 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Skipping scenarios\n\n Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.\n\n @skip\n Scenario: Skipping from a Before hook\n Given a step that is skipped\n\n Scenario: Skipping from a step doesn't affect the previous steps\n Given a step that does not skip\n And I skip a step\n\n Scenario: Skipping from a step causes the rest of the scenario to be skipped\n Given I skip a step\n And a step that is skipped\n","uri":"samples/skipped/skipped.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Skipping scenarios","description":" Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.","children":[{"scenario":{"id":"2","tags":[{"location":{"line":9,"column":3},"name":"@skip","id":"1"}],"location":{"line":10,"column":3},"keyword":"Scenario","name":"Skipping from a Before hook","description":"","steps":[{"id":"0","location":{"line":11,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is skipped"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":13,"column":3},"keyword":"Scenario","name":"Skipping from a step doesn't affect the previous steps","description":"","steps":[{"id":"3","location":{"line":14,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that does not skip"},{"id":"4","location":{"line":15,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"I skip a step"}],"examples":[]}},{"scenario":{"id":"8","tags":[],"location":{"line":17,"column":3},"keyword":"Scenario","name":"Skipping from a step causes the rest of the scenario to be skipped","description":"","steps":[{"id":"6","location":{"line":18,"column":5},"keyword":"Given ","keywordType":"Context","text":"I skip a step"},{"id":"7","location":{"line":19,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a step that is skipped"}],"examples":[]}}]},"comments":[],"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"id":"10","uri":"samples/skipped/skipped.feature","astNodeIds":["2"],"tags":[{"name":"@skip","astNodeId":"1"}],"name":"Skipping from a Before hook","language":"en","steps":[{"id":"9","text":"a step that is skipped","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"13","uri":"samples/skipped/skipped.feature","astNodeIds":["5"],"tags":[],"name":"Skipping from a step doesn't affect the previous steps","language":"en","steps":[{"id":"11","text":"a step that does not skip","type":"Context","astNodeIds":["3"]},{"id":"12","text":"I skip a step","type":"Context","astNodeIds":["4"]}]}} +{"pickle":{"id":"16","uri":"samples/skipped/skipped.feature","astNodeIds":["8"],"tags":[],"name":"Skipping from a step causes the rest of the scenario to be skipped","language":"en","steps":[{"id":"14","text":"I skip a step","type":"Context","astNodeIds":["6"]},{"id":"15","text":"a step that is skipped","type":"Context","astNodeIds":["7"]}]}} +{"stepDefinition":{"id":"18","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that does not skip"},"sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"19","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that is skipped"},"sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":11}}}} +{"stepDefinition":{"id":"20","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I skip a step"},"sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":15}}}} +{"hook":{"id":"17","type":"BEFORE_TEST_CASE","tagExpression":"@skip","sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"21","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"22","pickleId":"10","testSteps":[{"id":"23","hookId":"17"},{"id":"24","pickleStepId":"9","stepDefinitionIds":["19"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"21"}} +{"testCase":{"id":"25","pickleId":"13","testSteps":[{"id":"26","pickleStepId":"11","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"27","pickleStepId":"12","stepDefinitionIds":["20"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"21"}} +{"testCase":{"id":"28","pickleId":"16","testSteps":[{"id":"29","pickleStepId":"14","stepDefinitionIds":["20"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"30","pickleStepId":"15","stepDefinitionIds":["19"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"21"}} +{"testCaseStarted":{"id":"31","testCaseId":"22","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"23","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"23","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"24","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"24","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"seconds":0,"nanos":6000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"32","testCaseId":"25","timestamp":{"seconds":0,"nanos":7000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"26","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"26","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"27","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"27","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"33","testCaseId":"28","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"29","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"29","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"30","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"30","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":18000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"21","timestamp":{"seconds":0,"nanos":19000000},"success":true}} diff --git a/testdata/src/stack-traces.ndjson b/testdata/src/stack-traces.ndjson new file mode 100644 index 0000000..c646fee --- /dev/null +++ b/testdata/src/stack-traces.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Stack traces\n Stack traces can help you diagnose the source of a bug.\n\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default.\n\n The first line of the stack trace will contain a reference to the feature file.\n\n Scenario: A failing step\n When a step throws an exception\n","uri":"samples/stack-traces/stack-traces.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Stack traces","description":" Stack traces can help you diagnose the source of a bug.\n\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default.\n\n The first line of the stack trace will contain a reference to the feature file.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"A failing step","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"When ","keywordType":"Action","text":"a step throws an exception"}],"examples":[]}}]},"comments":[],"uri":"samples/stack-traces/stack-traces.feature"}} +{"pickle":{"id":"3","uri":"samples/stack-traces/stack-traces.feature","astNodeIds":["1"],"tags":[],"name":"A failing step","language":"en","steps":[{"id":"2","text":"a step throws an exception","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step throws an exception"},"sourceReference":{"uri":"samples/stack-traces/stack-traces.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"message":"BOOM","exception":{"type":"Error","message":"BOOM","stackTrace":"Error: BOOM\nsamples/stack-traces/stack-traces.feature:10"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/testdata/src/undefined.ndjson b/testdata/src/undefined.ndjson new file mode 100644 index 0000000..59e8c46 --- /dev/null +++ b/testdata/src/undefined.ndjson @@ -0,0 +1,39 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Undefined steps\n\n At runtime, Cucumber may encounter a step in a scenario that it cannot match to a step definition.\n\n In these cases, the scenario is not able to run and so the step status will be UNDEFINED, with\n subsequent steps being SKIPPED and the overall result will be FAILURE.\n\n Scenario: An undefined step causes a failure\n Given a step that is yet to be defined\n\n Scenario: Steps before undefined steps are executed\n Given an implemented step\n And a step that is yet to be defined\n\n Scenario: Steps after undefined steps are skipped\n Given a step that is yet to be defined\n And a step that will be skipped\n\n Scenario: Snippets reflect parameter types\n Given a list of 8 things\n","uri":"samples/undefined/undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Undefined steps","description":" At runtime, Cucumber may encounter a step in a scenario that it cannot match to a step definition.\n\n In these cases, the scenario is not able to run and so the step status will be UNDEFINED, with\n subsequent steps being SKIPPED and the overall result will be FAILURE.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":8,"column":3},"keyword":"Scenario","name":"An undefined step causes a failure","description":"","steps":[{"id":"0","location":{"line":9,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is yet to be defined"}],"examples":[]}},{"scenario":{"id":"4","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Steps before undefined steps are executed","description":"","steps":[{"id":"2","location":{"line":12,"column":5},"keyword":"Given ","keywordType":"Context","text":"an implemented step"},{"id":"3","location":{"line":13,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a step that is yet to be defined"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":15,"column":3},"keyword":"Scenario","name":"Steps after undefined steps are skipped","description":"","steps":[{"id":"5","location":{"line":16,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is yet to be defined"},{"id":"6","location":{"line":17,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a step that will be skipped"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":19,"column":3},"keyword":"Scenario","name":"Snippets reflect parameter types","description":"","steps":[{"id":"8","location":{"line":20,"column":5},"keyword":"Given ","keywordType":"Context","text":"a list of 8 things"}],"examples":[]}}]},"comments":[],"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"id":"11","uri":"samples/undefined/undefined.feature","astNodeIds":["1"],"tags":[],"name":"An undefined step causes a failure","language":"en","steps":[{"id":"10","text":"a step that is yet to be defined","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"14","uri":"samples/undefined/undefined.feature","astNodeIds":["4"],"tags":[],"name":"Steps before undefined steps are executed","language":"en","steps":[{"id":"12","text":"an implemented step","type":"Context","astNodeIds":["2"]},{"id":"13","text":"a step that is yet to be defined","type":"Context","astNodeIds":["3"]}]}} +{"pickle":{"id":"17","uri":"samples/undefined/undefined.feature","astNodeIds":["7"],"tags":[],"name":"Steps after undefined steps are skipped","language":"en","steps":[{"id":"15","text":"a step that is yet to be defined","type":"Context","astNodeIds":["5"]},{"id":"16","text":"a step that will be skipped","type":"Context","astNodeIds":["6"]}]}} +{"pickle":{"id":"19","uri":"samples/undefined/undefined.feature","astNodeIds":["9"],"tags":[],"name":"Snippets reflect parameter types","language":"en","steps":[{"id":"18","text":"a list of 8 things","type":"Context","astNodeIds":["8"]}]}} +{"stepDefinition":{"id":"20","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented step"},"sourceReference":{"uri":"samples/undefined/undefined.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"21","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that will be skipped"},"sourceReference":{"uri":"samples/undefined/undefined.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"22","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"23","pickleId":"11","testSteps":[{"id":"24","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"22"}} +{"testCase":{"id":"25","pickleId":"14","testSteps":[{"id":"26","pickleStepId":"12","stepDefinitionIds":["20"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"27","pickleStepId":"13","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"22"}} +{"testCase":{"id":"28","pickleId":"17","testSteps":[{"id":"29","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"30","pickleStepId":"16","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"22"}} +{"testCase":{"id":"31","pickleId":"19","testSteps":[{"id":"32","pickleStepId":"18","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"22"}} +{"testCaseStarted":{"id":"33","testCaseId":"23","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"24","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"34","pickleStepId":"10","snippets":[{"language":"typescript","code":"Given(\"a step that is yet to be defined\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"24","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"35","testCaseId":"25","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"35","testStepId":"26","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"35","testStepId":"26","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"35","testStepId":"27","timestamp":{"seconds":0,"nanos":8000000}}} +{"suggestion":{"id":"36","pickleStepId":"13","snippets":[{"language":"typescript","code":"Given(\"a step that is yet to be defined\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"35","testStepId":"27","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"35","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"37","testCaseId":"28","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"37","testStepId":"29","timestamp":{"seconds":0,"nanos":12000000}}} +{"suggestion":{"id":"38","pickleStepId":"15","snippets":[{"language":"typescript","code":"Given(\"a step that is yet to be defined\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"37","testStepId":"29","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"37","testStepId":"30","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"37","testStepId":"30","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"37","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"39","testCaseId":"31","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"32","timestamp":{"seconds":0,"nanos":18000000}}} +{"suggestion":{"id":"40","pickleStepId":"18","snippets":[{"language":"typescript","code":"Given(\"a list of {int} things\", (int: number) => {\n return \"pending\"\n})"},{"language":"typescript","code":"Given(\"a list of {float} things\", (float: number) => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"32","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"39","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"22","timestamp":{"seconds":0,"nanos":21000000},"success":false}} diff --git a/testdata/src/unknown-parameter-type.ndjson b/testdata/src/unknown-parameter-type.ndjson new file mode 100644 index 0000000..0bd3002 --- /dev/null +++ b/testdata/src/unknown-parameter-type.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run. Additionally, because\n the step is effectively undefined, a suggestion will also be created.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Parameter Types","description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run. Additionally, because\n the step is effectively undefined, a suggestion will also be created.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"undefined parameter type","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"CDG is closed because of a strike"}],"examples":[]}}]},"comments":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"pickle":{"id":"3","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature","astNodeIds":["1"],"tags":[],"name":"undefined parameter type","language":"en","steps":[{"id":"2","text":"CDG is closed because of a strike","type":"Context","astNodeIds":["0"]}]}} +{"undefinedParameterType":{"name":"airport","expression":"{airport} is closed because of a strike"}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"9","pickleStepId":"2","snippets":[{"language":"typescript","code":"Given(\"CDG is closed because of a strike\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/testdata/src/unused-steps.json b/testdata/src/unused-steps.json new file mode 100644 index 0000000..b179d3f --- /dev/null +++ b/testdata/src/unused-steps.json @@ -0,0 +1,25 @@ +{ + "stepDefinitions": [ + { + "expression": "a step that is used", + "location": "samples/unused-steps/unused-steps.ts:3", + "duration": { + "sum": 0.001000000, + "mean": 0.001000000, + "moe95": 0.0 + }, + "steps": [ + { + "text": "a step that is used", + "duration": 0.001000000, + "location": "samples/unused-steps/unused-steps.feature:5" + } + ] + }, + { + "expression": "a step that is not used", + "location": "samples/unused-steps/unused-steps.ts:7", + "steps": [ ] + } + ] +} \ No newline at end of file diff --git a/testdata/src/unused-steps.ndjson b/testdata/src/unused-steps.ndjson new file mode 100644 index 0000000..39182bd --- /dev/null +++ b/testdata/src/unused-steps.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"29.0.1","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Unused steps\n Depending on the run, some step definitions may not be used. This is valid, and the step definitions are still\n includes in the stream of messages, which allows formatters to report on step usage if desired.\n\n Scenario: a scenario\n Given a step that is used\n","uri":"samples/unused-steps/unused-steps.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Unused steps","description":" Depending on the run, some step definitions may not be used. This is valid, and the step definitions are still\n includes in the stream of messages, which allows formatters to report on step usage if desired.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is used"}],"examples":[]}}]},"comments":[],"uri":"samples/unused-steps/unused-steps.feature"}} +{"pickle":{"id":"3","uri":"samples/unused-steps/unused-steps.feature","astNodeIds":["1"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"2","text":"a step that is used","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that is used"},"sourceReference":{"uri":"samples/unused-steps/unused-steps.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that is not used"},"sourceReference":{"uri":"samples/unused-steps/unused-steps.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/testdata/src/unused-steps.plain.txt b/testdata/src/unused-steps.plain.txt new file mode 100644 index 0000000..30a2f3c --- /dev/null +++ b/testdata/src/unused-steps.plain.txt @@ -0,0 +1,6 @@ + +Expression/Text Duration Mean ± Error Location +a step that is used 0.001s 0.001s ± 0.000s samples/unused-steps/unused-steps.ts:3 + a step that is used 0.001s samples/unused-steps/unused-steps.feature:5 +a step that is not used samples/unused-steps/unused-steps.ts:7 + UNUSED From a70aa89edeca7dd1c7f28f6392925a0f7f272ff9 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 11 Oct 2025 00:15:58 +0200 Subject: [PATCH 02/13] Make usage report configurable --- CHANGELOG.md | 4 +- README.md | 42 +++- java/pom.xml | 2 +- .../usageformatter/MessagesToUsageWriter.java | 2 +- .../usageformatter/PlainTextSerializer.java | 151 ------------- .../UsageReportPlainTextSerializer.java | 206 ++++++++++++++++++ .../MessagesToUsageWriterAcceptanceTest.java | 13 +- .../MessagesToUsageWriterTest.java | 67 ++++++ testdata/README.md | 6 +- testdata/src/minimal.step-definitions.txt | 3 + ...nimal.plain.txt => minimal.with-steps.txt} | 0 .../multiple-features.step-definitions.txt | 3 + ...n.txt => multiple-features.with-steps.txt} | 0 .../src/unused-steps.step-definitions.txt | 4 + ....plain.txt => unused-steps.with-steps.txt} | 0 15 files changed, 336 insertions(+), 167 deletions(-) delete mode 100644 java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java create mode 100644 java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java create mode 100644 testdata/src/minimal.step-definitions.txt rename testdata/src/{minimal.plain.txt => minimal.with-steps.txt} (100%) create mode 100644 testdata/src/multiple-features.step-definitions.txt rename testdata/src/{multiple-features.plain.txt => multiple-features.with-steps.txt} (100%) create mode 100644 testdata/src/unused-steps.step-definitions.txt rename testdata/src/{unused-steps.plain.txt => unused-steps.with-steps.txt} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c2312..cd50557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Java implementation ([#1](https://github.com/cucumber/pretty-formatter/pull/1) M.P. Korstanje) +- Java implementation ([#1](https://github.com/cucumber/usage-formatter/pull/1) M.P. Korstanje) -[Unreleased]: https://github.com/cucumber/pretty-formatter/compare/f21831df98c5e57a53950a92b068df8321e45bce...HEAD +[Unreleased]: https://github.com/cucumber/usage-formatter/compare/f21831df98c5e57a53950a92b068df8321e45bce...HEAD diff --git a/README.md b/README.md index 58c0314..cd92e2b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,49 @@ # Usage Formatter -⚠️ This is an internal package; you don't need to install it in order to use the JSON Formatter. +⚠️ This is an internal package; you don't need to install it in order to use the Usage Formatter. -[![Maven Central](https://img.shields.io/maven-central/v/io.cucumber/cucumber-json-formatter.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:io.cucumber%20AND%20a:cucumber-json-formatter) +[![Maven Central](https://img.shields.io/maven-central/v/io.cucumber/cucumber-json-formatter.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:io.cucumber%20AND%20a:usage-formatter) Writes usage statistics for step definitions ## Features -TODO: +Can render a plain text report showing which step definitions are used by which +steps and some statistics for long each took. The error from the mean is the 95% +confidence interval assuming a normal distribution. + +``` +Expression/Text Duration Mean ± Error Location +an order for {string} 0.009s 0.001s ± 0.000s samples/multiple-features/multiple-features.ts:3 + an order for "eggs" 0.001s samples/multiple-features/multiple-features-1.feature:3 + an order for "milk" 0.001s samples/multiple-features/multiple-features-1.feature:6 + an order for "bread" 0.001s samples/multiple-features/multiple-features-1.feature:9 + an order for "batteries" 0.001s samples/multiple-features/multiple-features-2.feature:3 + an order for "light bulbs" 0.001s samples/multiple-features/multiple-features-2.feature:6 + 4 more +``` + +The output can also be rendered as a json report. + +```json +{ + "stepDefinitions": [ + { + "expression": "an order for {string}", + "location": "samples/multiple-features/multiple-features.ts:3", + "duration": { + "sum": 0.009000000, + "mean": 0.001000000, + "moe95": 0.0 + }, + "steps": [ + { + "text": "an order for \"eggs\"", + "duration": 0.001000000, + "location": "samples/multiple-features/multiple-features-1.feature:3" + }, +... +``` + ## Contributing diff --git a/java/pom.xml b/java/pom.xml index 4baf088..7ac1fa6 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -8,7 +8,7 @@ 4.4.0 - pretty-formatter + usage-formatter 0.0.1-SNAPSHOT jar Usage Formatter diff --git a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java index 595632d..c2092b0 100644 --- a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java +++ b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java @@ -16,7 +16,7 @@ import static java.util.Objects.requireNonNull; /** - * Creates a usage report for step definitions based on a test run. + * Writes usage statistics for step definitions. *

* Note: Messages are first collected and only written once the stream is * closed. diff --git a/java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java b/java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java deleted file mode 100644 index c98ca91..0000000 --- a/java/src/main/java/io/cucumber/usageformatter/PlainTextSerializer.java +++ /dev/null @@ -1,151 +0,0 @@ -package io.cucumber.usageformatter; - -import io.cucumber.usageformatter.UsageReport.Statistics; -import io.cucumber.usageformatter.UsageReport.StepDefinitionUsage; - -import java.io.IOException; -import java.io.Writer; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.StringJoiner; - -import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; -import static java.lang.System.lineSeparator; -import static java.math.RoundingMode.HALF_EVEN; -import static java.util.Comparator.comparing; -import static java.util.Comparator.nullsFirst; - -public final class PlainTextSerializer implements MessagesToUsageWriter.Serializer { - - // TODO: Make inclusion of steps configurable - // TODO: Make configurable? - public static final int MAX_NUMBER_OF_STEPS = 5; - public static final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; - public static final boolean[] leftAlignHeader = {true, false, false, true, false, true}; - - // TODO: Builder - PlainTextSerializer() { - - } - - @Override - public void writeValue(Writer writer, UsageReport value) throws IOException { - writer.append(format(value)); - } - - private static String format(UsageReport usageReport) { - List stepDefinitions = usageReport.getStepDefinitions(); - - if (stepDefinitions.isEmpty()) { - return ""; - } - - List table = new ArrayList<>(); - table.add(headers); - - stepDefinitions - .stream() - .sorted( - comparing(StepDefinitionUsage::getDuration, nullsFirst(comparing(Statistics::getMean))).reversed()) - .forEach(stepDefinitionUsage -> { - Statistics duration = stepDefinitionUsage.getDuration(); - table.add(new String[]{ - stepDefinitionUsage.getExpression(), - duration == null ? "" : formatDuration(duration.getSum()), - duration == null ? "" : formatDuration(duration.getMean()), - duration == null ? "" : "±", - duration == null ? "" : formatDuration(duration.getMoe95()), - stepDefinitionUsage.getLocation() - }); - - if (stepDefinitionUsage.getSteps().isEmpty()) { - table.add(new String[]{ - " UNUSED", - "", - "", - "", - "", - "" - }); - } else { - stepDefinitionUsage.getSteps().stream() - .sorted(comparing(UsageReport.StepUsage::getDuration).reversed()) - .limit(5) - .forEach(stepUsage -> - table.add(new String[]{ - " " + stepUsage.getText(), - formatDuration(stepUsage.getDuration()), - "", - "", - "", - stepUsage.getLocation() - })); - if (stepDefinitionUsage.getSteps().size() > MAX_NUMBER_OF_STEPS) { - table.add(new String[]{ - " " + (stepDefinitionUsage.getSteps().size() - MAX_NUMBER_OF_STEPS) + " more", - "", - "", - "", - "", - "" - }); - } - } - - }); - - StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); - int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); - for (String[] row : table) { - StringJoiner rowJoiner = new StringJoiner(" "); - for (int j = 0; j < row.length; j++) { - String newElement = renderCellWithPadding( - longestCellLengthInColumn[j], - row[j], - leftAlignHeader[j] - ); - rowJoiner.add(newElement); - } - joiner.add(rowJoiner.toString()); - } - return joiner.toString(); - } - - private static String formatDuration(Duration duration) { - return toBigDecimalSeconds(duration).setScale(3, HALF_EVEN).toPlainString() + "s"; - } - - private static int[] findLongestCellLengthInColumn(List renderedCells) { - // always square and non-sparse. - int width = renderedCells.get(0).length; - int[] longestCellInColumnLength = new int[width]; - for (String[] row : renderedCells) { - for (int colIndex = 0; colIndex < width; colIndex++) { - int current = longestCellInColumnLength[colIndex]; - int candidate = row[colIndex].length(); - longestCellInColumnLength[colIndex] = Math.max(current, candidate); - } - } - return longestCellInColumnLength; - } - - private static String renderCellWithPadding(int width, String cell, boolean leftAlign) { - StringBuilder result = new StringBuilder(); - if (leftAlign) { - result.append(cell); - padSpace(result, width - cell.length()); - } else { - padSpace(result, width - cell.length()); - result.append(cell); - } - return result.toString(); - } - - private static void padSpace(StringBuilder result, int padding) { - for (int i = 0; i < padding; i++) { - result.append(" "); - } - } - -} diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java new file mode 100644 index 0000000..4e2a045 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java @@ -0,0 +1,206 @@ +package io.cucumber.usageformatter; + +import io.cucumber.usageformatter.UsageReport.Statistics; +import io.cucumber.usageformatter.UsageReport.StepDefinitionUsage; + +import java.io.IOException; +import java.io.Writer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; +import static java.lang.System.lineSeparator; +import static java.math.RoundingMode.HALF_EVEN; +import static java.util.Comparator.comparing; +import static java.util.Comparator.nullsFirst; + +public final class UsageReportPlainTextSerializer implements MessagesToUsageWriter.Serializer { + + public final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; + public final boolean[] leftAlignHeader = {true, false, false, true, false, true}; + public final int maxStepsPerStepDefinition; + private final Set features; + + private UsageReportPlainTextSerializer(int maxStepsPerStepDefinition, Set features) { + this.maxStepsPerStepDefinition = maxStepsPerStepDefinition; + this.features = features; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public void writeValue(Writer writer, UsageReport value) throws IOException { + writer.append(format(value)); + } + + private String format(UsageReport usageReport) { + List stepDefinitions = usageReport.getStepDefinitions(); + if (stepDefinitions.isEmpty()) { + return ""; + } + return formatTable(createTable(stepDefinitions)); + } + + private List createTable(List stepDefinitions) { + List table = new ArrayList<>(); + table.add(headers); + + stepDefinitions + .stream() + .sorted(byMeanDurationDescending()) + .forEach(stepDefinitionUsage -> { + Statistics duration = stepDefinitionUsage.getDuration(); + table.add(new String[]{ + stepDefinitionUsage.getExpression(), + duration == null ? "" : formatDuration(duration.getSum()), + duration == null ? "" : formatDuration(duration.getMean()), + duration == null ? "" : "±", + duration == null ? "" : formatDuration(duration.getMoe95()), + stepDefinitionUsage.getLocation() + }); + + if (features.contains(PlainTextFeature.INCLUDE_STEPS)) { + + if (stepDefinitionUsage.getSteps().isEmpty()) { + table.add(new String[]{ + " UNUSED", + "", + "", + "", + "", + "" + }); + } else { + stepDefinitionUsage.getSteps().stream() + .sorted(comparing(UsageReport.StepUsage::getDuration).reversed()) + .limit(maxStepsPerStepDefinition) + .forEach(stepUsage -> + table.add(new String[]{ + " " + stepUsage.getText(), + formatDuration(stepUsage.getDuration()), + "", + "", + "", + stepUsage.getLocation() + })); + if (stepDefinitionUsage.getSteps().size() > maxStepsPerStepDefinition) { + table.add(new String[]{ + " " + (stepDefinitionUsage.getSteps().size() - maxStepsPerStepDefinition) + " more", + "", + "", + "", + "", + "" + }); + } + } + } + + }); + return table; + } + + private String formatTable(List table) { + StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); + int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); + for (String[] row : table) { + StringJoiner rowJoiner = new StringJoiner(" "); + for (int j = 0; j < row.length; j++) { + String newElement = renderCellWithPadding( + longestCellLengthInColumn[j], + row[j], + leftAlignHeader[j] + ); + rowJoiner.add(newElement); + } + joiner.add(rowJoiner.toString()); + } + return joiner.toString(); + } + + private static Comparator byMeanDurationDescending() { + return comparing(StepDefinitionUsage::getDuration, nullsFirst(comparing(Statistics::getMean))).reversed(); + } + + private String formatDuration(Duration duration) { + return toBigDecimalSeconds(duration).setScale(3, HALF_EVEN).toPlainString() + "s"; + } + + private static int[] findLongestCellLengthInColumn(List renderedCells) { + // always square and non-sparse. + int width = renderedCells.get(0).length; + int[] longestCellInColumnLength = new int[width]; + for (String[] row : renderedCells) { + for (int colIndex = 0; colIndex < width; colIndex++) { + int current = longestCellInColumnLength[colIndex]; + int candidate = row[colIndex].length(); + longestCellInColumnLength[colIndex] = Math.max(current, candidate); + } + } + return longestCellInColumnLength; + } + + private static String renderCellWithPadding(int width, String cell, boolean leftAlign) { + StringBuilder result = new StringBuilder(); + if (leftAlign) { + result.append(cell); + padSpace(result, width - cell.length()); + } else { + padSpace(result, width - cell.length()); + result.append(cell); + } + return result.toString(); + } + + private static void padSpace(StringBuilder result, int padding) { + for (int i = 0; i < padding; i++) { + result.append(" "); + } + } + + public static final class Builder { + private final Set features = EnumSet.noneOf(PlainTextFeature.class); + private int maxStepsPerStepDefinition = 0; + + /** + * Toggles a given feature. + */ + public Builder feature(PlainTextFeature feature, boolean enabled) { + if (enabled) { + features.add(feature); + } else { + features.remove(feature); + } + return this; + } + + /** + * Limit the number of steps shown per step definition. + *

+ * A negative value means all steps are included. + */ + public Builder maxStepsPerStepDefinition(int n) { + this.maxStepsPerStepDefinition = n < 0 ? Integer.MAX_VALUE : n; + return this; + } + + public UsageReportPlainTextSerializer build() { + return new UsageReportPlainTextSerializer(maxStepsPerStepDefinition, features); + } + } + + public enum PlainTextFeature { + + /** + * Include steps using a step definition. + */ + INCLUDE_STEPS, + } +} diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java index d23d6c5..69fe91e 100644 --- a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java @@ -29,12 +29,15 @@ class MessagesToUsageWriterAcceptanceTest { private static final NdjsonToMessageIterable.Deserializer deserializer = (json) -> OBJECT_MAPPER.readValue(json, Envelope.class); private static final MessagesToUsageWriter.Serializer jsonSerializer = OBJECT_MAPPER.writer(PRETTY_PRINTER)::writeValue; - private static final MessagesToUsageWriter.Serializer plainTextSerializer = new PlainTextSerializer(); static List acceptance() throws IOException { Map formats = new LinkedHashMap<>(); formats.put("json", builder(jsonSerializer)); - formats.put("plain.txt", builder(plainTextSerializer)); + formats.put("step-definitions.txt", builder(UsageReportPlainTextSerializer.builder().build())); + formats.put("with-steps.txt", builder(UsageReportPlainTextSerializer.builder() + .feature(UsageReportPlainTextSerializer.PlainTextFeature.INCLUDE_STEPS, true) + .maxStepsPerStepDefinition(5) + .build())); ; List sources = getSources(); @@ -56,7 +59,7 @@ private static List getSources() { } - private static T writePrettyReport(TestCase testCase, T out, Builder builder) throws IOException { + private static T writeUsageReport(TestCase testCase, T out, Builder builder) throws IOException { try (InputStream in = Files.newInputStream(testCase.source)) { try (NdjsonToMessageIterable envelopes = new NdjsonToMessageIterable(in, deserializer)) { try (MessagesToUsageWriter writer = builder.build(out)) { @@ -72,7 +75,7 @@ private static T writePrettyReport(TestCase testCase, T @ParameterizedTest @MethodSource("acceptance") void test(TestCase testCase) throws IOException { - ByteArrayOutputStream bytes = writePrettyReport(testCase, new ByteArrayOutputStream(), testCase.builder); + ByteArrayOutputStream bytes = writeUsageReport(testCase, new ByteArrayOutputStream(), testCase.builder); assertThat(bytes.toString()).isEqualToIgnoringNewLines(new String(readAllBytes(testCase.expected))); } @@ -81,7 +84,7 @@ void test(TestCase testCase) throws IOException { @Disabled void updateExpectedFiles(TestCase testCase) throws IOException { try (OutputStream out = Files.newOutputStream(testCase.expected)) { - writePrettyReport(testCase, out, testCase.builder); + writeUsageReport(testCase, out, testCase.builder); if (!testCase.format.equals("json")) { // Render output in console, easier to inspect results Files.copy(testCase.expected, System.out); diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java new file mode 100644 index 0000000..892b8c1 --- /dev/null +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java @@ -0,0 +1,67 @@ +package io.cucumber.usageformatter; + +import io.cucumber.messages.types.Envelope; +import io.cucumber.messages.types.TestRunFinished; +import io.cucumber.messages.types.TestRunStarted; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Instant; + +import static io.cucumber.messages.Convertor.toMessage; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MessagesToUsageWriterTest { + + @Test + void it_writes_two_messages_to_usage() throws IOException { + Instant started = Instant.ofEpochSecond(10); + Instant finished = Instant.ofEpochSecond(30); + + String out = renderAsSummary( + Envelope.of(new TestRunStarted(toMessage(started), "some-id")), + Envelope.of(new TestRunFinished(null, true, toMessage(finished), null, "some-id"))); + + assertThat(out).isEmpty(); + } + + @Test + void it_writes_no_message_to_summary() throws IOException { + String out = renderAsSummary(); + assertThat(out).isEmpty(); + } + + @Test + void it_throws_when_writing_after_close() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MessagesToUsageWriter writer = create(bytes); + writer.close(); + assertThrows(IOException.class, () -> writer.write(null)); + } + + @Test + void it_can_be_closed_twice() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MessagesToUsageWriter writer = create(bytes); + writer.close(); + assertDoesNotThrow(writer::close); + } + + private static String renderAsSummary(Envelope... messages) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (MessagesToUsageWriter writer = create(bytes)) { + for (Envelope message : messages) { + writer.write(message); + } + } + return new String(bytes.toByteArray(), UTF_8); + } + + private static MessagesToUsageWriter create(ByteArrayOutputStream bytes) { + return MessagesToUsageWriter.builder(UsageReportPlainTextSerializer.builder().build()).build(bytes); + } +} diff --git a/testdata/README.md b/testdata/README.md index 2feec25..c960bb7 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -1,6 +1,6 @@ # Acceptance test data -The pretty formatter uses the examples from the [cucumber compatibility kit](https://github.com/cucumber/compatibility-kit) +The usage formatter uses the examples from the [cucumber compatibility kit](https://github.com/cucumber/compatibility-kit) for acceptance testing. These examples consist of `.ndjson` files created by the [`fake-cucumber` reference implementation](https://github.com/cucumber/fake-cucumber). @@ -11,6 +11,4 @@ We ensure the `.ndjson` files stay up to date by running `npm install` in CI and verifying nothing changed. Should there be changes, these tests can be used to update the expected data: - * Java: `MessagesToPrettyWriterAcceptanceTest#updateExpectedFiles` - * Java: `MessagesToSummaryWriterAcceptanceTest#updateExpectedFiles` - * Java: `MessagesToProgressWriterAcceptanceTest#updateExpectedFiles` + * Java: `MessagesToUsageWriterAcceptanceTest#updateExpectedFiles` diff --git a/testdata/src/minimal.step-definitions.txt b/testdata/src/minimal.step-definitions.txt new file mode 100644 index 0000000..3c45d2b --- /dev/null +++ b/testdata/src/minimal.step-definitions.txt @@ -0,0 +1,3 @@ + +Expression/Text Duration Mean ± Error Location +I have {int} cukes in my belly 0.001s 0.001s ± 0.000s samples/minimal/minimal.ts:3 diff --git a/testdata/src/minimal.plain.txt b/testdata/src/minimal.with-steps.txt similarity index 100% rename from testdata/src/minimal.plain.txt rename to testdata/src/minimal.with-steps.txt diff --git a/testdata/src/multiple-features.step-definitions.txt b/testdata/src/multiple-features.step-definitions.txt new file mode 100644 index 0000000..101ede9 --- /dev/null +++ b/testdata/src/multiple-features.step-definitions.txt @@ -0,0 +1,3 @@ + +Expression/Text Duration Mean ± Error Location +an order for {string} 0.009s 0.001s ± 0.000s samples/multiple-features/multiple-features.ts:3 diff --git a/testdata/src/multiple-features.plain.txt b/testdata/src/multiple-features.with-steps.txt similarity index 100% rename from testdata/src/multiple-features.plain.txt rename to testdata/src/multiple-features.with-steps.txt diff --git a/testdata/src/unused-steps.step-definitions.txt b/testdata/src/unused-steps.step-definitions.txt new file mode 100644 index 0000000..53e0743 --- /dev/null +++ b/testdata/src/unused-steps.step-definitions.txt @@ -0,0 +1,4 @@ + +Expression/Text Duration Mean ± Error Location +a step that is used 0.001s 0.001s ± 0.000s samples/unused-steps/unused-steps.ts:3 +a step that is not used samples/unused-steps/unused-steps.ts:7 diff --git a/testdata/src/unused-steps.plain.txt b/testdata/src/unused-steps.with-steps.txt similarity index 100% rename from testdata/src/unused-steps.plain.txt rename to testdata/src/unused-steps.with-steps.txt From a843f69d7d0ffadabdcdd83175d8edfe3b3d042a Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 11 Oct 2025 00:23:36 +0200 Subject: [PATCH 03/13] Add unused formatter --- .../UnusedReportSerializer.java | 33 +++++++++++++++++++ ...alizer.java => UsageReportSerializer.java} | 8 ++--- .../MessagesToUsageWriterAcceptanceTest.java | 11 +++---- .../MessagesToUsageWriterTest.java | 2 +- testdata/src/minimal.unused.txt | 2 ++ testdata/src/multiple-features.unused.txt | 2 ++ testdata/src/unused-steps.unused.txt | 3 ++ 7 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java rename java/src/main/java/io/cucumber/usageformatter/{UsageReportPlainTextSerializer.java => UsageReportSerializer.java} (95%) create mode 100644 testdata/src/minimal.unused.txt create mode 100644 testdata/src/multiple-features.unused.txt create mode 100644 testdata/src/unused-steps.unused.txt diff --git a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java new file mode 100644 index 0000000..e1425a6 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java @@ -0,0 +1,33 @@ +package io.cucumber.usageformatter; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; +import java.util.StringJoiner; + +import static java.lang.System.lineSeparator; +import static java.util.stream.Collectors.toList; + +public final class UnusedReportSerializer implements MessagesToUsageWriter.Serializer { + + @Override + public void writeValue(Writer writer, UsageReport value) throws IOException { + writer.append(format(value)); + } + + private String format(UsageReport usageReport) { + List stepDefinitions = usageReport.getStepDefinitions(); + List unusedSteps = stepDefinitions.stream() + .filter(stepDefinitionUsage -> stepDefinitionUsage.getSteps().isEmpty()) + .collect(toList()); + + StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); + joiner.add(unusedSteps.size() + " unused step definition:"); + unusedSteps.forEach(entry -> { + String location = entry.getLocation(); + String pattern = entry.getExpression(); + joiner.add(location + " # " + pattern); + }); + return joiner.toString(); + } +} diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java similarity index 95% rename from java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java rename to java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java index 4e2a045..03d9b6b 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportPlainTextSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java @@ -19,14 +19,14 @@ import static java.util.Comparator.comparing; import static java.util.Comparator.nullsFirst; -public final class UsageReportPlainTextSerializer implements MessagesToUsageWriter.Serializer { +public final class UsageReportSerializer implements MessagesToUsageWriter.Serializer { public final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; public final boolean[] leftAlignHeader = {true, false, false, true, false, true}; public final int maxStepsPerStepDefinition; private final Set features; - private UsageReportPlainTextSerializer(int maxStepsPerStepDefinition, Set features) { + private UsageReportSerializer(int maxStepsPerStepDefinition, Set features) { this.maxStepsPerStepDefinition = maxStepsPerStepDefinition; this.features = features; } @@ -191,8 +191,8 @@ public Builder maxStepsPerStepDefinition(int n) { return this; } - public UsageReportPlainTextSerializer build() { - return new UsageReportPlainTextSerializer(maxStepsPerStepDefinition, features); + public UsageReportSerializer build() { + return new UsageReportSerializer(maxStepsPerStepDefinition, features); } } diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java index 69fe91e..c9bdf95 100644 --- a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java @@ -30,15 +30,15 @@ class MessagesToUsageWriterAcceptanceTest { private static final NdjsonToMessageIterable.Deserializer deserializer = (json) -> OBJECT_MAPPER.readValue(json, Envelope.class); private static final MessagesToUsageWriter.Serializer jsonSerializer = OBJECT_MAPPER.writer(PRETTY_PRINTER)::writeValue; - static List acceptance() throws IOException { + static List acceptance() { Map formats = new LinkedHashMap<>(); formats.put("json", builder(jsonSerializer)); - formats.put("step-definitions.txt", builder(UsageReportPlainTextSerializer.builder().build())); - formats.put("with-steps.txt", builder(UsageReportPlainTextSerializer.builder() - .feature(UsageReportPlainTextSerializer.PlainTextFeature.INCLUDE_STEPS, true) + formats.put("unused.txt", builder(new UnusedReportSerializer())); + formats.put("step-definitions.txt", builder(UsageReportSerializer.builder().build())); + formats.put("with-steps.txt", builder(UsageReportSerializer.builder() + .feature(UsageReportSerializer.PlainTextFeature.INCLUDE_STEPS, true) .maxStepsPerStepDefinition(5) .build())); - ; List sources = getSources(); @@ -56,7 +56,6 @@ private static List getSources() { Paths.get("../testdata/src/unused-steps.ndjson"), Paths.get("../testdata/src/multiple-features.ndjson") ); - } private static T writeUsageReport(TestCase testCase, T out, Builder builder) throws IOException { diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java index 892b8c1..945b891 100644 --- a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java @@ -62,6 +62,6 @@ private static String renderAsSummary(Envelope... messages) throws IOException { } private static MessagesToUsageWriter create(ByteArrayOutputStream bytes) { - return MessagesToUsageWriter.builder(UsageReportPlainTextSerializer.builder().build()).build(bytes); + return MessagesToUsageWriter.builder(UsageReportSerializer.builder().build()).build(bytes); } } diff --git a/testdata/src/minimal.unused.txt b/testdata/src/minimal.unused.txt new file mode 100644 index 0000000..a27a271 --- /dev/null +++ b/testdata/src/minimal.unused.txt @@ -0,0 +1,2 @@ + +0 unused step definition: diff --git a/testdata/src/multiple-features.unused.txt b/testdata/src/multiple-features.unused.txt new file mode 100644 index 0000000..a27a271 --- /dev/null +++ b/testdata/src/multiple-features.unused.txt @@ -0,0 +1,2 @@ + +0 unused step definition: diff --git a/testdata/src/unused-steps.unused.txt b/testdata/src/unused-steps.unused.txt new file mode 100644 index 0000000..fa36092 --- /dev/null +++ b/testdata/src/unused-steps.unused.txt @@ -0,0 +1,3 @@ + +1 unused step definition: +samples/unused-steps/unused-steps.ts:7 # a step that is not used From e1cc66b6736d47959e9b59bb06df8038d9f10a2f Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 11 Oct 2025 00:48:28 +0200 Subject: [PATCH 04/13] Be consistent --- .../usageformatter/UsageReportSerializer.java | 118 ++++++++++-------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java index 03d9b6b..20d26ea 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java @@ -2,6 +2,7 @@ import io.cucumber.usageformatter.UsageReport.Statistics; import io.cucumber.usageformatter.UsageReport.StepDefinitionUsage; +import io.cucumber.usageformatter.UsageReport.StepUsage; import java.io.IOException; import java.io.Writer; @@ -14,6 +15,7 @@ import java.util.StringJoiner; import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; +import static io.cucumber.usageformatter.UsageReportSerializer.PlainTextFeature.INCLUDE_STEPS; import static java.lang.System.lineSeparator; import static java.math.RoundingMode.HALF_EVEN; import static java.util.Comparator.comparing; @@ -21,6 +23,7 @@ public final class UsageReportSerializer implements MessagesToUsageWriter.Serializer { + private static final int INCLUDE_ALL_STEPS = -1; public final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; public final boolean[] leftAlignHeader = {true, false, false, true, false, true}; public final int maxStepsPerStepDefinition; @@ -55,58 +58,71 @@ private List createTable(List stepDefinitions) { stepDefinitions .stream() .sorted(byMeanDurationDescending()) - .forEach(stepDefinitionUsage -> { - Statistics duration = stepDefinitionUsage.getDuration(); - table.add(new String[]{ - stepDefinitionUsage.getExpression(), - duration == null ? "" : formatDuration(duration.getSum()), - duration == null ? "" : formatDuration(duration.getMean()), - duration == null ? "" : "±", - duration == null ? "" : formatDuration(duration.getMoe95()), - stepDefinitionUsage.getLocation() - }); - - if (features.contains(PlainTextFeature.INCLUDE_STEPS)) { - - if (stepDefinitionUsage.getSteps().isEmpty()) { - table.add(new String[]{ - " UNUSED", - "", - "", - "", - "", - "" - }); - } else { - stepDefinitionUsage.getSteps().stream() - .sorted(comparing(UsageReport.StepUsage::getDuration).reversed()) - .limit(maxStepsPerStepDefinition) - .forEach(stepUsage -> - table.add(new String[]{ - " " + stepUsage.getText(), - formatDuration(stepUsage.getDuration()), - "", - "", - "", - stepUsage.getLocation() - })); - if (stepDefinitionUsage.getSteps().size() > maxStepsPerStepDefinition) { - table.add(new String[]{ - " " + (stepDefinitionUsage.getSteps().size() - maxStepsPerStepDefinition) + " more", - "", - "", - "", - "", - "" - }); - } - } - } - - }); + .map(this::createRows) + .forEach(table::addAll); + return table; } + private List createRows(StepDefinitionUsage stepDefinitionUsage) { + List rows = new ArrayList<>(); + Statistics duration = stepDefinitionUsage.getDuration(); + + // Add step definition row + rows.add(new String[]{ + stepDefinitionUsage.getExpression(), + duration == null ? "" : formatDuration(duration.getSum()), + duration == null ? "" : formatDuration(duration.getMean()), + duration == null ? "" : "±", + duration == null ? "" : formatDuration(duration.getMoe95()), + stepDefinitionUsage.getLocation() + }); + + if (!features.contains(INCLUDE_STEPS)) { + return rows; + } + + // Add rows for steps, if any + List steps = stepDefinitionUsage.getSteps(); + if (steps.isEmpty()) { + rows.add(new String[]{ + " UNUSED", + "", + "", + "", + "", + "" + }); + return rows; + } + + steps.sort(comparing(StepUsage::getDuration).reversed()); + boolean includeAllSteps = maxStepsPerStepDefinition == INCLUDE_ALL_STEPS; + int includeToIndex = includeAllSteps ? steps.size() : Math.min(maxStepsPerStepDefinition, steps.size()); + for (StepUsage stepUsage : steps.subList(0, includeToIndex)) { + rows.add(new String[]{ + " " + stepUsage.getText(), + formatDuration(stepUsage.getDuration()), + "", + "", + "", + stepUsage.getLocation() + }); + } + if (steps.size() > includeToIndex) { + rows.add(new String[]{ + " " + (steps.size() - includeToIndex) + " more", + "", + "", + "", + "", + "" + }); + } + + return rows; + } + private String formatTable(List table) { StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); @@ -167,7 +183,7 @@ private static void padSpace(StringBuilder result, int padding) { public static final class Builder { private final Set features = EnumSet.noneOf(PlainTextFeature.class); - private int maxStepsPerStepDefinition = 0; + private int maxStepsPerStepDefinition = INCLUDE_ALL_STEPS; /** * Toggles a given feature. @@ -187,7 +203,7 @@ public Builder feature(PlainTextFeature feature, boolean enabled) { * A negative value means all steps are included. */ public Builder maxStepsPerStepDefinition(int n) { - this.maxStepsPerStepDefinition = n < 0 ? Integer.MAX_VALUE : n; + this.maxStepsPerStepDefinition = n < 0 ? INCLUDE_ALL_STEPS : n; return this; } From 969adc73fb33cce3e7cd529ec9a3026b8447cd47 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 11 Oct 2025 00:51:15 +0200 Subject: [PATCH 05/13] Be efficient? --- .../usageformatter/UsageReportSerializer.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java index 20d26ea..45a02e1 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java @@ -96,19 +96,22 @@ private List createRows(StepDefinitionUsage stepDefinitionUsage) { return rows; } - steps.sort(comparing(StepUsage::getDuration).reversed()); boolean includeAllSteps = maxStepsPerStepDefinition == INCLUDE_ALL_STEPS; int includeToIndex = includeAllSteps ? steps.size() : Math.min(maxStepsPerStepDefinition, steps.size()); - for (StepUsage stepUsage : steps.subList(0, includeToIndex)) { - rows.add(new String[]{ - " " + stepUsage.getText(), - formatDuration(stepUsage.getDuration()), - "", - "", - "", - stepUsage.getLocation() - }); - } + + steps.stream() + .sorted(comparing(StepUsage::getDuration).reversed()) + .limit(includeToIndex) + .forEach(stepUsage -> + rows.add(new String[]{ + " " + stepUsage.getText(), + formatDuration(stepUsage.getDuration()), + "", + "", + "", + stepUsage.getLocation() + })); + if (steps.size() > includeToIndex) { rows.add(new String[]{ " " + (steps.size() - includeToIndex) + " more", @@ -119,7 +122,7 @@ private List createRows(StepDefinitionUsage stepDefinitionUsage) { "" }); } - + return rows; } From 5c31137caa623e2c8c21a26a8dc5a720c92cfdb7 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 11 Oct 2025 00:52:53 +0200 Subject: [PATCH 06/13] Text --- .../java/io/cucumber/usageformatter/UnusedReportSerializer.java | 2 +- testdata/src/minimal.unused.txt | 2 +- testdata/src/multiple-features.unused.txt | 2 +- testdata/src/unused-steps.unused.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java index e1425a6..01a4e3c 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java @@ -22,7 +22,7 @@ private String format(UsageReport usageReport) { .collect(toList()); StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); - joiner.add(unusedSteps.size() + " unused step definition:"); + joiner.add(unusedSteps.size() + " unused step definition(s):"); unusedSteps.forEach(entry -> { String location = entry.getLocation(); String pattern = entry.getExpression(); diff --git a/testdata/src/minimal.unused.txt b/testdata/src/minimal.unused.txt index a27a271..b6ff4db 100644 --- a/testdata/src/minimal.unused.txt +++ b/testdata/src/minimal.unused.txt @@ -1,2 +1,2 @@ -0 unused step definition: +0 unused step definition(s): diff --git a/testdata/src/multiple-features.unused.txt b/testdata/src/multiple-features.unused.txt index a27a271..b6ff4db 100644 --- a/testdata/src/multiple-features.unused.txt +++ b/testdata/src/multiple-features.unused.txt @@ -1,2 +1,2 @@ -0 unused step definition: +0 unused step definition(s): diff --git a/testdata/src/unused-steps.unused.txt b/testdata/src/unused-steps.unused.txt index fa36092..ea3f42e 100644 --- a/testdata/src/unused-steps.unused.txt +++ b/testdata/src/unused-steps.unused.txt @@ -1,3 +1,3 @@ -1 unused step definition: +1 unused step definition(s): samples/unused-steps/unused-steps.ts:7 # a step that is not used From fa7c2d4e1ceb3a372758ec6a66ad8356ab840d5c Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 12 Oct 2025 01:09:18 +0200 Subject: [PATCH 07/13] Extract table and formatter --- .../io/cucumber/usageformatter/Table.java | 41 ++++++++ .../usageformatter/TableFormatter.java | 57 +++++++++++ .../UnusedReportSerializer.java | 22 +++-- .../usageformatter/UsageReportSerializer.java | 94 ++++--------------- testdata/src/minimal.unused.txt | 2 +- testdata/src/multiple-features.unused.txt | 2 +- testdata/src/unused-steps.unused.txt | 4 +- 7 files changed, 136 insertions(+), 86 deletions(-) create mode 100644 java/src/main/java/io/cucumber/usageformatter/Table.java create mode 100644 java/src/main/java/io/cucumber/usageformatter/TableFormatter.java diff --git a/java/src/main/java/io/cucumber/usageformatter/Table.java b/java/src/main/java/io/cucumber/usageformatter/Table.java new file mode 100644 index 0000000..41de693 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/Table.java @@ -0,0 +1,41 @@ +package io.cucumber.usageformatter; + +import java.util.ArrayList; +import java.util.List; + +final class Table { + + private final List rows = new ArrayList<>(); + + Table() { + } + + Table(String... headers) { + this.rows.add(headers); + } + + void add(String... row) { + this.rows.add(row); + } + + + void addAll(List rows) { + this.rows.addAll(rows); + } + + List getRows() { + return rows; + } + + int width() { + // assumed to be square and non-sparse. + return getRows().get(0).length; + } + + static Table addTo(Table a, Table b) { + a.addAll(b.rows); + return a; + } + + +} diff --git a/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java b/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java new file mode 100644 index 0000000..0d27c23 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java @@ -0,0 +1,57 @@ +package io.cucumber.usageformatter; + +import java.util.StringJoiner; + +import static java.lang.System.lineSeparator; + +final class TableFormatter { + + static String format(Table table, boolean[] leftAlignColumn) { + StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); + int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); + for (String[] row : table.getRows()) { + StringJoiner rowJoiner = new StringJoiner(" "); + for (int j = 0; j < row.length; j++) { + String newElement = renderCellWithPadding( + longestCellLengthInColumn[j], + row[j], + leftAlignColumn[j] + ); + rowJoiner.add(newElement); + } + joiner.add(rowJoiner.toString()); + } + return joiner.toString(); + } + + private static int[] findLongestCellLengthInColumn(Table renderedCells) { + int width = renderedCells.width(); + int[] longestCellInColumnLength = new int[width]; + for (String[] row : renderedCells.getRows()) { + for (int colIndex = 0; colIndex < width; colIndex++) { + int current = longestCellInColumnLength[colIndex]; + int candidate = row[colIndex].length(); + longestCellInColumnLength[colIndex] = Math.max(current, candidate); + } + } + return longestCellInColumnLength; + } + + private static String renderCellWithPadding(int width, String cell, boolean leftAlign) { + StringBuilder result = new StringBuilder(); + if (leftAlign) { + result.append(cell); + padSpace(result, width - cell.length()); + } else { + padSpace(result, width - cell.length()); + result.append(cell); + } + return result.toString(); + } + + private static void padSpace(StringBuilder result, int padding) { + for (int i = 0; i < padding; i++) { + result.append(" "); + } + } +} diff --git a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java index 01a4e3c..edbc8fd 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java @@ -9,6 +9,8 @@ import static java.util.stream.Collectors.toList; public final class UnusedReportSerializer implements MessagesToUsageWriter.Serializer { + private final String[] headers = {"Location", "Expression"}; + private final boolean[] leftAlignColumn = {true, true}; @Override public void writeValue(Writer writer, UsageReport value) throws IOException { @@ -17,17 +19,21 @@ public void writeValue(Writer writer, UsageReport value) throws IOException { private String format(UsageReport usageReport) { List stepDefinitions = usageReport.getStepDefinitions(); - List unusedSteps = stepDefinitions.stream() + List unusedStepDefinitions = stepDefinitions.stream() .filter(stepDefinitionUsage -> stepDefinitionUsage.getSteps().isEmpty()) .collect(toList()); - StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); - joiner.add(unusedSteps.size() + " unused step definition(s):"); - unusedSteps.forEach(entry -> { - String location = entry.getLocation(); - String pattern = entry.getExpression(); - joiner.add(location + " # " + pattern); - }); + StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), ""); + joiner.add(unusedStepDefinitions.size() + " unused step definition(s)"); + + Table table = new Table(headers); + unusedStepDefinitions.forEach(entry -> + table.add(entry.getLocation(), "# " + entry.getExpression())); + + if (table.getRows().size() > 1) { + joiner.add(TableFormatter.format(table, leftAlignColumn)); + } + return joiner.toString(); } } diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java index 45a02e1..0056107 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java @@ -7,16 +7,13 @@ import java.io.IOException; import java.io.Writer; import java.time.Duration; -import java.util.ArrayList; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.Set; -import java.util.StringJoiner; import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; import static io.cucumber.usageformatter.UsageReportSerializer.PlainTextFeature.INCLUDE_STEPS; -import static java.lang.System.lineSeparator; import static java.math.RoundingMode.HALF_EVEN; import static java.util.Comparator.comparing; import static java.util.Comparator.nullsFirst; @@ -25,7 +22,7 @@ public final class UsageReportSerializer implements MessagesToUsageWriter.Serial private static final int INCLUDE_ALL_STEPS = -1; public final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; - public final boolean[] leftAlignHeader = {true, false, false, true, false, true}; + public final boolean[] leftAlignColumn = {true, false, false, true, false, true}; public final int maxStepsPerStepDefinition; private final Set features; @@ -48,52 +45,48 @@ private String format(UsageReport usageReport) { if (stepDefinitions.isEmpty()) { return ""; } - return formatTable(createTable(stepDefinitions)); + Table table = createTable(stepDefinitions); + return TableFormatter.format(table, leftAlignColumn); } - private List createTable(List stepDefinitions) { - List table = new ArrayList<>(); - table.add(headers); - - stepDefinitions + private Table createTable(List stepDefinitions) { + return stepDefinitions .stream() .sorted(byMeanDurationDescending()) .map(this::createRows) - .forEach(table::addAll); - - return table; + .reduce(new Table(headers), Table::addTo); } - private List createRows(StepDefinitionUsage stepDefinitionUsage) { - List rows = new ArrayList<>(); + private Table createRows(StepDefinitionUsage stepDefinitionUsage) { + Table table = new Table(); Statistics duration = stepDefinitionUsage.getDuration(); // Add step definition row - rows.add(new String[]{ + table.add( stepDefinitionUsage.getExpression(), duration == null ? "" : formatDuration(duration.getSum()), duration == null ? "" : formatDuration(duration.getMean()), duration == null ? "" : "±", duration == null ? "" : formatDuration(duration.getMoe95()), stepDefinitionUsage.getLocation() - }); + ); if (!features.contains(INCLUDE_STEPS)) { - return rows; + return table; } // Add rows for steps, if any List steps = stepDefinitionUsage.getSteps(); if (steps.isEmpty()) { - rows.add(new String[]{ + table.add( " UNUSED", "", "", "", "", "" - }); - return rows; + ); + return table; } boolean includeAllSteps = maxStepsPerStepDefinition == INCLUDE_ALL_STEPS; @@ -103,45 +96,27 @@ private List createRows(StepDefinitionUsage stepDefinitionUsage) { .sorted(comparing(StepUsage::getDuration).reversed()) .limit(includeToIndex) .forEach(stepUsage -> - rows.add(new String[]{ + table.add( " " + stepUsage.getText(), formatDuration(stepUsage.getDuration()), "", "", "", stepUsage.getLocation() - })); + )); if (steps.size() > includeToIndex) { - rows.add(new String[]{ + table.add( " " + (steps.size() - includeToIndex) + " more", "", "", "", "", "" - }); + ); } - return rows; - } - - private String formatTable(List table) { - StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); - int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); - for (String[] row : table) { - StringJoiner rowJoiner = new StringJoiner(" "); - for (int j = 0; j < row.length; j++) { - String newElement = renderCellWithPadding( - longestCellLengthInColumn[j], - row[j], - leftAlignHeader[j] - ); - rowJoiner.add(newElement); - } - joiner.add(rowJoiner.toString()); - } - return joiner.toString(); + return table; } private static Comparator byMeanDurationDescending() { @@ -152,37 +127,6 @@ private String formatDuration(Duration duration) { return toBigDecimalSeconds(duration).setScale(3, HALF_EVEN).toPlainString() + "s"; } - private static int[] findLongestCellLengthInColumn(List renderedCells) { - // always square and non-sparse. - int width = renderedCells.get(0).length; - int[] longestCellInColumnLength = new int[width]; - for (String[] row : renderedCells) { - for (int colIndex = 0; colIndex < width; colIndex++) { - int current = longestCellInColumnLength[colIndex]; - int candidate = row[colIndex].length(); - longestCellInColumnLength[colIndex] = Math.max(current, candidate); - } - } - return longestCellInColumnLength; - } - - private static String renderCellWithPadding(int width, String cell, boolean leftAlign) { - StringBuilder result = new StringBuilder(); - if (leftAlign) { - result.append(cell); - padSpace(result, width - cell.length()); - } else { - padSpace(result, width - cell.length()); - result.append(cell); - } - return result.toString(); - } - - private static void padSpace(StringBuilder result, int padding) { - for (int i = 0; i < padding; i++) { - result.append(" "); - } - } public static final class Builder { private final Set features = EnumSet.noneOf(PlainTextFeature.class); diff --git a/testdata/src/minimal.unused.txt b/testdata/src/minimal.unused.txt index b6ff4db..f7e64d7 100644 --- a/testdata/src/minimal.unused.txt +++ b/testdata/src/minimal.unused.txt @@ -1,2 +1,2 @@ -0 unused step definition(s): +0 unused step definition(s) \ No newline at end of file diff --git a/testdata/src/multiple-features.unused.txt b/testdata/src/multiple-features.unused.txt index b6ff4db..f7e64d7 100644 --- a/testdata/src/multiple-features.unused.txt +++ b/testdata/src/multiple-features.unused.txt @@ -1,2 +1,2 @@ -0 unused step definition(s): +0 unused step definition(s) \ No newline at end of file diff --git a/testdata/src/unused-steps.unused.txt b/testdata/src/unused-steps.unused.txt index ea3f42e..d480129 100644 --- a/testdata/src/unused-steps.unused.txt +++ b/testdata/src/unused-steps.unused.txt @@ -1,3 +1,5 @@ -1 unused step definition(s): +1 unused step definition(s) + +Location Expression samples/unused-steps/unused-steps.ts:7 # a step that is not used From 0097fffcc62d7b9a829a31b973415ac1d844db25 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 13 Oct 2025 04:08:18 +0200 Subject: [PATCH 08/13] Update docs so I don't forget --- .../main/java/io/cucumber/usageformatter/Durations.java | 5 +++++ .../main/java/io/cucumber/usageformatter/UsageReport.java | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/java/src/main/java/io/cucumber/usageformatter/Durations.java b/java/src/main/java/io/cucumber/usageformatter/Durations.java index 976f8cb..db8d135 100644 --- a/java/src/main/java/io/cucumber/usageformatter/Durations.java +++ b/java/src/main/java/io/cucumber/usageformatter/Durations.java @@ -22,6 +22,11 @@ static UsageReport.Statistics createStatistics(List durations) { /** * Calculate the margin of error with a 0.95% confidence interval. + *

+ * So assuming a normal distribution, the duration of a step will fall + * within {@code mean ± moe95} with 95% probability. + * + * @see Wikipedia - Margin of error */ private static Duration calculateMarginOfError95(List durations, Duration mean) { BigDecimal meanSeconds = toBigDecimalSeconds(mean); diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java index 5ff03d8..7890bbb 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java @@ -72,7 +72,12 @@ public Duration getMean() { } /** - * Margin of error with a 95% confidence interval, assuming a normal distribution. + * Margin of error with a 95% confidence interval. + *

+ * So assuming a normal distribution, the duration of a step will fall + * within {@code mean ± moe95} with 95% probability. + * + * @see Wikipedia - Margin of error */ public Duration getMoe95() { return moe95; From 6a78a4d182f06f507809fd76f0e82ee8e6393f43 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 13 Oct 2025 04:10:19 +0200 Subject: [PATCH 09/13] Update docs so I don't forget --- java/src/main/java/io/cucumber/usageformatter/Durations.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/main/java/io/cucumber/usageformatter/Durations.java b/java/src/main/java/io/cucumber/usageformatter/Durations.java index db8d135..0c42b00 100644 --- a/java/src/main/java/io/cucumber/usageformatter/Durations.java +++ b/java/src/main/java/io/cucumber/usageformatter/Durations.java @@ -36,7 +36,7 @@ private static Duration calculateMarginOfError95(List durations, Durat .reduce(BigDecimal::add) .orElse(BigDecimal.ZERO); // TODO: With Java 17, use BigDecimal.sqrt and - // BigDecimal.divideAndRemainder for seconds and nos + // BigDecimal.divideAndRemainder for seconds and nanos double marginOfError = 2 * Math.sqrt(variance.doubleValue()) / durations.size(); long seconds = (long) Math.floor(marginOfError); long nanos = (long) Math.floor((marginOfError - seconds) * TimeUnit.SECONDS.toNanos(1)); From 82ed34811a6a2c02517f981907e41b84340cc59e Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 14 Oct 2025 03:32:53 +0200 Subject: [PATCH 10/13] Make json output more like usage-json --- .../io/cucumber/usageformatter/Durations.java | 17 ++- .../usageformatter/MessagesToUsageWriter.java | 32 +---- .../UnusedReportSerializer.java | 11 +- .../cucumber/usageformatter/UsageReport.java | 55 +++++--- .../usageformatter/UsageReportBuilder.java | 74 ++++------ .../usageformatter/UsageReportSerializer.java | 54 ++++++-- .../usageformatter/DurationsTest.java | 39 +++--- .../MessagesToUsageWriterAcceptanceTest.java | 1 + testdata/src/ambiguous.json | 30 ++++ testdata/src/ambiguous.step-definitions.txt | 4 + testdata/src/ambiguous.unused.txt | 6 + testdata/src/ambiguous.with-steps.txt | 6 + testdata/src/minimal.json | 42 ++++-- testdata/src/multiple-features.json | 130 ++++++++++++++---- testdata/src/unused-steps.json | 56 ++++++-- 15 files changed, 382 insertions(+), 175 deletions(-) create mode 100644 testdata/src/ambiguous.json create mode 100644 testdata/src/ambiguous.step-definitions.txt create mode 100644 testdata/src/ambiguous.unused.txt create mode 100644 testdata/src/ambiguous.with-steps.txt diff --git a/java/src/main/java/io/cucumber/usageformatter/Durations.java b/java/src/main/java/io/cucumber/usageformatter/Durations.java index 0c42b00..9cc1cc4 100644 --- a/java/src/main/java/io/cucumber/usageformatter/Durations.java +++ b/java/src/main/java/io/cucumber/usageformatter/Durations.java @@ -1,12 +1,15 @@ package io.cucumber.usageformatter; +import io.cucumber.messages.Convertor; +import io.cucumber.usageformatter.UsageReport.Statistics; + import java.math.BigDecimal; import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; final class Durations { - static UsageReport.Statistics createStatistics(List durations) { + static Statistics createStatistics(List durations) { if (durations.isEmpty()) { return null; } @@ -17,7 +20,11 @@ static UsageReport.Statistics createStatistics(List durations) { .orElse(Duration.ZERO); Duration mean = sum.dividedBy(durations.size()); Duration moe95 = calculateMarginOfError95(durations, mean); - return new UsageReport.Statistics(sum, mean, moe95); + return new Statistics( + Convertor.toMessage(sum), + Convertor.toMessage(mean), + Convertor.toMessage(moe95) + ); } /** @@ -36,8 +43,8 @@ private static Duration calculateMarginOfError95(List durations, Durat .reduce(BigDecimal::add) .orElse(BigDecimal.ZERO); // TODO: With Java 17, use BigDecimal.sqrt and - // BigDecimal.divideAndRemainder for seconds and nanos double marginOfError = 2 * Math.sqrt(variance.doubleValue()) / durations.size(); + // TODO: With Java 17, BigDecimal.divideAndRemainder for seconds and nanos long seconds = (long) Math.floor(marginOfError); long nanos = (long) Math.floor((marginOfError - seconds) * TimeUnit.SECONDS.toNanos(1)); return Duration.ofSeconds(seconds, nanos); @@ -46,4 +53,8 @@ private static Duration calculateMarginOfError95(List durations, Durat static BigDecimal toBigDecimalSeconds(Duration duration) { return BigDecimal.valueOf(duration.getSeconds()).add(BigDecimal.valueOf(duration.getNano(), 9)); } + + static BigDecimal toBigDecimalSeconds(io.cucumber.messages.types.Duration duration) { + return BigDecimal.valueOf(duration.getSeconds()).add(BigDecimal.valueOf(duration.getNanos(), 9)); + } } diff --git a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java index c2092b0..42e7aed 100644 --- a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java +++ b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java @@ -9,7 +9,6 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; -import java.util.function.Function; import static io.cucumber.query.Repository.RepositoryFeature.INCLUDE_GHERKIN_DOCUMENTS; import static io.cucumber.query.Repository.RepositoryFeature.INCLUDE_STEP_DEFINITIONS; @@ -30,15 +29,13 @@ public final class MessagesToUsageWriter implements AutoCloseable { .build(); private final Query query = new Query(repository); private final Serializer serializer; - private final Function uriFormatter; private boolean streamClosed = false; - MessagesToUsageWriter(OutputStream out, Serializer serializer, Function uriFormatter) { + MessagesToUsageWriter(OutputStream out, Serializer serializer) { this.out = new OutputStreamWriter( requireNonNull(out), StandardCharsets.UTF_8); this.serializer = requireNonNull(serializer); - this.uriFormatter = requireNonNull(uriFormatter); } public void write(Envelope envelope) throws IOException { @@ -54,37 +51,14 @@ public static Builder builder(Serializer serializer) { public static final class Builder { private final Serializer serializer; - private Function uriFormatter = Function.identity(); private Builder(Serializer serializer) { this.serializer = requireNonNull(serializer); } - private static Function removePrefix(String prefix) { - // TODO: Needs coverage - return s -> { - if (s.startsWith(prefix)) { - return s.substring(prefix.length()); - } - return s; - }; - } - - /** - * Removes a given prefix from all URI locations. - *

- * The typical usage would be to trim the current working directory. - * This makes the report more readable. - */ - public Builder removeUriPrefix(String prefix) { - // TODO: Needs coverage - this.uriFormatter = removePrefix(requireNonNull(prefix)); - return this; - } - public MessagesToUsageWriter build(OutputStream out) { requireNonNull(out); - return new MessagesToUsageWriter(out, serializer, uriFormatter); + return new MessagesToUsageWriter(out, serializer); } } @@ -94,7 +68,7 @@ public void close() throws IOException { return; } try { - UsageReport report = new UsageReportBuilder(query, uriFormatter).build(); + UsageReport report = new UsageReportBuilder(query).build(); serializer.writeValue(out, report); } finally { try { diff --git a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java index edbc8fd..db3f4d2 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UnusedReportSerializer.java @@ -4,11 +4,13 @@ import java.io.Writer; import java.util.List; import java.util.StringJoiner; +import java.util.function.Function; import static java.lang.System.lineSeparator; import static java.util.stream.Collectors.toList; public final class UnusedReportSerializer implements MessagesToUsageWriter.Serializer { + private final SourceReferenceFormatter sourceReferenceFormatter = new SourceReferenceFormatter(Function.identity()); private final String[] headers = {"Location", "Expression"}; private final boolean[] leftAlignColumn = {true, true}; @@ -20,15 +22,18 @@ public void writeValue(Writer writer, UsageReport value) throws IOException { private String format(UsageReport usageReport) { List stepDefinitions = usageReport.getStepDefinitions(); List unusedStepDefinitions = stepDefinitions.stream() - .filter(stepDefinitionUsage -> stepDefinitionUsage.getSteps().isEmpty()) + .filter(stepDefinitionUsage -> stepDefinitionUsage.getMatches().isEmpty()) .collect(toList()); StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), ""); joiner.add(unusedStepDefinitions.size() + " unused step definition(s)"); Table table = new Table(headers); - unusedStepDefinitions.forEach(entry -> - table.add(entry.getLocation(), "# " + entry.getExpression())); + unusedStepDefinitions.forEach(entry -> { + String source = sourceReferenceFormatter.format(entry.getSourceReference()).orElse(""); + String expression = entry.getExpression().getSource(); + table.add(source, "# " + expression); + }); if (table.getRows().size() > 1) { joiner.add(TableFormatter.format(table, leftAlignColumn)); diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java index 7890bbb..df7beff 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java @@ -1,7 +1,12 @@ package io.cucumber.usageformatter; -import java.time.Duration; +import io.cucumber.messages.types.Duration; +import io.cucumber.messages.types.Location; +import io.cucumber.messages.types.SourceReference; +import io.cucumber.messages.types.StepDefinitionPattern; + import java.util.List; +import java.util.Optional; import static java.util.Objects.requireNonNull; @@ -21,34 +26,34 @@ public List getStepDefinitions() { */ public static final class StepDefinitionUsage { - private final String expression; - private final String location; + private final StepDefinitionPattern pattern; + private final SourceReference sourceReference; private final Statistics duration; - private final List steps; + private final List matches; StepDefinitionUsage( - String expression, String location, Statistics duration, List steps + StepDefinitionPattern pattern, SourceReference sourceReference, Statistics duration, List matches ) { - this.expression = requireNonNull(expression); - this.location = requireNonNull(location); + this.pattern = requireNonNull(pattern); + this.sourceReference = requireNonNull(sourceReference); this.duration = duration; - this.steps = requireNonNull(steps); + this.matches = requireNonNull(matches); } - public String getExpression() { - return expression; + public StepDefinitionPattern getExpression() { + return pattern; } public Statistics getDuration() { return duration; } - public List getSteps() { - return steps; + public List getMatches() { + return matches; } - public String getLocation() { - return location; + public SourceReference getSourceReference() { + return sourceReference; } } @@ -72,11 +77,11 @@ public Duration getMean() { } /** - * Margin of error with a 95% confidence interval. + * Margin of error with a 95% confidence interval. *

* So assuming a normal distribution, the duration of a step will fall - * within {@code mean ± moe95} with 95% probability. - * + * within {@code mean ± moe95} with 95% probability. + * * @see Wikipedia - Margin of error */ public Duration getMoe95() { @@ -88,20 +93,26 @@ public static final class StepUsage { private final String text; private final Duration duration; - private final String location; + private final String uri; + private final Location location; - StepUsage(String text, Duration duration, String location) { + StepUsage(String text, Duration duration, String uri, Location location) { this.text = requireNonNull(text); this.duration = requireNonNull(duration); - this.location = requireNonNull(location); + this.uri = requireNonNull(uri); + this.location = location; } public Duration getDuration() { return duration; } - public String getLocation() { - return location; + public String getUri() { + return uri; + } + + public Optional getLocation() { + return Optional.ofNullable(location); } public String getText() { diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java index 6a37c79..a1edb61 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java @@ -1,11 +1,10 @@ package io.cucumber.usageformatter; import io.cucumber.messages.Convertor; -import io.cucumber.messages.types.Location; -import io.cucumber.messages.types.PickleStep; import io.cucumber.messages.types.StepDefinition; import io.cucumber.messages.types.TestStepFinished; import io.cucumber.query.Query; +import io.cucumber.usageformatter.UsageReport.StepUsage; import java.time.Duration; import java.util.ArrayList; @@ -23,21 +22,16 @@ final class UsageReportBuilder { private final Query query; - private final Function uriFormatter; - private final SourceReferenceFormatter sourceReferenceFormatter; - UsageReportBuilder(Query query, Function uriFormatter) { + UsageReportBuilder(Query query) { this.query = requireNonNull(query); - this.uriFormatter = requireNonNull(uriFormatter); - this.sourceReferenceFormatter = new SourceReferenceFormatter(uriFormatter); } UsageReport build() { - Map, List> testStepsFinishedByStepDefinition = query + Map, List>> testStepsFinishedByStepDefinition = query .findAllTestStepFinished() .stream() - .collect(groupingBy(findUnambiguousStepDefinitionBy(), LinkedHashMap::new, - mapping(createStepUsage(), toList()))); + .collect(groupingBy(findUnambiguousStepDefinitionBy(), LinkedHashMap::new, mapping(createStepUsage(), toList()))); // Add unused step definitions query.findAllStepDefinitions().stream() @@ -50,57 +44,40 @@ UsageReport build() { // Filter out steps with without a step definition or with an // ambiguous step definition. These can't be represented. .filter(entry -> entry.getKey().isPresent()) - .map(entry -> createStepDefinitionUsage(entry.getKey().get(), entry.getValue())) + .map(entry -> createStepDefinitionUsage(entry.getKey().get(), flatten(entry.getValue()))) .collect(toList()); return new UsageReport(stepDefinitionUsages); } - private UsageReport.StepDefinitionUsage createStepDefinitionUsage(StepDefinition stepDefinition, List stepUsages) { + private UsageReport.StepDefinitionUsage createStepDefinitionUsage(StepDefinition stepDefinition, List matches) { return new UsageReport.StepDefinitionUsage( - formatSource(stepDefinition), - formatSourceReference(stepDefinition), - createStatistics(stepUsages), - stepUsages + stepDefinition.getPattern(), + stepDefinition.getSourceReference(), + createStatistics(matches), + matches ); } - private String formatSource(StepDefinition stepDefinition) { - return stepDefinition.getPattern().getSource(); - } - - private String formatSourceReference(StepDefinition stepDefinition) { - return sourceReferenceFormatter.format(stepDefinition.getSourceReference()).orElse(""); - } - - private UsageReport.Statistics createStatistics(List stepUsages) { + private UsageReport.Statistics createStatistics(List stepUsages) { List durations = stepUsages.stream() - .map(UsageReport.StepUsage::getDuration) + .map(StepUsage::getDuration) + .map(Convertor::toDuration) .collect(toList()); return Durations.createStatistics(durations); } - private Function createStepUsage() { - return testStepFinished -> query - .findTestStepBy(testStepFinished) + private Function> createStepUsage() { + return testStepFinished -> query.findTestStepBy(testStepFinished) .flatMap(query::findPickleStepBy) - .map(pickleStep -> createStepUsage(testStepFinished, pickleStep)) - .orElseGet(() -> new UsageReport.StepUsage("", Duration.ZERO, "")); - } - - private UsageReport.StepUsage createStepUsage(TestStepFinished testStepFinished, PickleStep pickleStep) { - String text = pickleStep.getText(); - String location = findLocationOf(testStepFinished); - Duration duration = Convertor.toDuration(testStepFinished.getTestStepResult().getDuration()); - return new UsageReport.StepUsage(text, duration, location); - } - - private String findLocationOf(TestStepFinished testStepFinished) { - return query.findPickleBy(testStepFinished) - .map(pickle -> uriFormatter.apply(pickle.getUri()) + query.findLocationOf(pickle) - .map(Location::getLine) - .map(line -> ":" + line) - .orElse("")) - .orElse(""); + .flatMap(pickleStep -> query + .findPickleBy(testStepFinished) + .map(pickle -> new StepUsage( + pickleStep.getText(), + testStepFinished.getTestStepResult().getDuration(), + pickle.getUri(), + query.findLocationOf(pickle).orElse(null) + ) + )); } private Function> findUnambiguousStepDefinitionBy() { @@ -108,4 +85,7 @@ private Function> findUnambiguousStep .flatMap(query::findUnambiguousStepDefinitionBy); } + private static List flatten(List> value) { + return value.stream().filter(Optional::isPresent).map(Optional::get).collect(toList()); + } } diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java index 0056107..fd18923 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java @@ -1,34 +1,43 @@ package io.cucumber.usageformatter; +import io.cucumber.messages.DurationComparator; +import io.cucumber.messages.types.Duration; +import io.cucumber.messages.types.Location; import io.cucumber.usageformatter.UsageReport.Statistics; import io.cucumber.usageformatter.UsageReport.StepDefinitionUsage; import io.cucumber.usageformatter.UsageReport.StepUsage; import java.io.IOException; import java.io.Writer; -import java.time.Duration; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import static io.cucumber.usageformatter.Durations.toBigDecimalSeconds; import static io.cucumber.usageformatter.UsageReportSerializer.PlainTextFeature.INCLUDE_STEPS; import static java.math.RoundingMode.HALF_EVEN; import static java.util.Comparator.comparing; import static java.util.Comparator.nullsFirst; +import static java.util.Objects.requireNonNull; public final class UsageReportSerializer implements MessagesToUsageWriter.Serializer { private static final int INCLUDE_ALL_STEPS = -1; + private static final DurationComparator durationComparator = new DurationComparator(); public final String[] headers = new String[]{"Expression/Text", "Duration", "Mean", "±", "Error", "Location"}; public final boolean[] leftAlignColumn = {true, false, false, true, false, true}; public final int maxStepsPerStepDefinition; private final Set features; + private final Function uriFormatter; + private final SourceReferenceFormatter sourceReferenceFormatter; - private UsageReportSerializer(int maxStepsPerStepDefinition, Set features) { + private UsageReportSerializer(int maxStepsPerStepDefinition, Set features, Function uriFormatter) { this.maxStepsPerStepDefinition = maxStepsPerStepDefinition; this.features = features; + this.uriFormatter = requireNonNull(uriFormatter); + this.sourceReferenceFormatter = new SourceReferenceFormatter(uriFormatter); } public static Builder builder() { @@ -63,12 +72,12 @@ private Table createRows(StepDefinitionUsage stepDefinitionUsage) { // Add step definition row table.add( - stepDefinitionUsage.getExpression(), + stepDefinitionUsage.getExpression().getSource(), duration == null ? "" : formatDuration(duration.getSum()), duration == null ? "" : formatDuration(duration.getMean()), duration == null ? "" : "±", duration == null ? "" : formatDuration(duration.getMoe95()), - stepDefinitionUsage.getLocation() + sourceReferenceFormatter.format(stepDefinitionUsage.getSourceReference()).orElse("") ); if (!features.contains(INCLUDE_STEPS)) { @@ -76,7 +85,7 @@ private Table createRows(StepDefinitionUsage stepDefinitionUsage) { } // Add rows for steps, if any - List steps = stepDefinitionUsage.getSteps(); + List steps = stepDefinitionUsage.getMatches(); if (steps.isEmpty()) { table.add( " UNUSED", @@ -93,7 +102,7 @@ private Table createRows(StepDefinitionUsage stepDefinitionUsage) { int includeToIndex = includeAllSteps ? steps.size() : Math.min(maxStepsPerStepDefinition, steps.size()); steps.stream() - .sorted(comparing(StepUsage::getDuration).reversed()) + .sorted(comparing(StepUsage::getDuration, durationComparator).reversed()) .limit(includeToIndex) .forEach(stepUsage -> table.add( @@ -102,7 +111,10 @@ private Table createRows(StepDefinitionUsage stepDefinitionUsage) { "", "", "", - stepUsage.getLocation() + uriFormatter.apply(stepUsage.getUri()) + stepUsage.getLocation() + .map(Location::getLine) + .map(line -> ":" + line) + .orElse("") )); if (steps.size() > includeToIndex) { @@ -120,7 +132,8 @@ private Table createRows(StepDefinitionUsage stepDefinitionUsage) { } private static Comparator byMeanDurationDescending() { - return comparing(StepDefinitionUsage::getDuration, nullsFirst(comparing(Statistics::getMean))).reversed(); + Comparator compareMean = comparing(Statistics::getMean, new DurationComparator()); + return comparing(StepDefinitionUsage::getDuration, nullsFirst(compareMean)).reversed(); } private String formatDuration(Duration duration) { @@ -131,6 +144,7 @@ private String formatDuration(Duration duration) { public static final class Builder { private final Set features = EnumSet.noneOf(PlainTextFeature.class); private int maxStepsPerStepDefinition = INCLUDE_ALL_STEPS; + private Function uriFormatter = Function.identity(); /** * Toggles a given feature. @@ -154,8 +168,30 @@ public Builder maxStepsPerStepDefinition(int n) { return this; } + /** + * Removes a given prefix from all URI locations. + *

+ * The typical usage would be to trim the current working directory. + * This makes the report more readable. + */ + public Builder removeUriPrefix(String prefix) { + // TODO: Needs coverage + this.uriFormatter = removePrefix(requireNonNull(prefix)); + return this; + } + + private static Function removePrefix(String prefix) { + // TODO: Needs coverage + return s -> { + if (s.startsWith(prefix)) { + return s.substring(prefix.length()); + } + return s; + }; + } + public UsageReportSerializer build() { - return new UsageReportSerializer(maxStepsPerStepDefinition, features); + return new UsageReportSerializer(maxStepsPerStepDefinition, features, uriFormatter); } } diff --git a/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java b/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java index 5419e9b..a32cb3d 100644 --- a/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java @@ -12,46 +12,53 @@ import static org.assertj.core.api.Assertions.assertThat; public class DurationsTest { - + @Test - void testToBigDecimalSeconds(){ + void testToBigDecimalSeconds() { assertThat(toBigDecimalSeconds(Duration.ofMillis(0))).isEqualTo(BigDecimal.valueOf(0, 9)); assertThat(toBigDecimalSeconds(Duration.ofMillis(100))).isEqualTo(BigDecimal.valueOf(100_000_000, 9)); assertThat(toBigDecimalSeconds(Duration.ofMillis(1000))).isEqualTo(BigDecimal.valueOf(1_000_000_000, 9)); + assertThat(toBigDecimalSeconds(createDuration(0L, 0L))).isEqualTo(BigDecimal.valueOf(0, 9)); + assertThat(toBigDecimalSeconds(createDuration(0L, 100_000_000L))).isEqualTo(BigDecimal.valueOf(100_000_000, 9)); + assertThat(toBigDecimalSeconds(createDuration(1L, 0))).isEqualTo(BigDecimal.valueOf(1_000_000_000, 9)); } - + @Test - void createStatistics_without_values(){ + void createStatistics_without_values() { Statistics statistics = Durations.createStatistics(Collections.emptyList()); assertThat(statistics).isNull(); } - + @Test - void createStatistics_with_even_number_of_values(){ + void createStatistics_with_even_number_of_values() { Statistics statistics = Durations.createStatistics(Arrays.asList( Duration.ofSeconds(1), Duration.ofSeconds(1), Duration.ofSeconds(2), Duration.ofSeconds(4) )); - + assertThat(statistics).isNotNull(); - assertThat(statistics.getSum()).isEqualTo(Duration.ofSeconds(8)); - assertThat(statistics.getMean()).isEqualTo(Duration.ofSeconds(2)); - assertThat(statistics.getMoe95()).isEqualTo(Duration.parse("PT1.224744871S")); + assertThat(statistics.getSum()).isEqualTo(createDuration(8L, 0L)); + assertThat(statistics.getMean()).isEqualTo(createDuration(2L, 0L)); + assertThat(statistics.getMoe95()).isEqualTo(createDuration(1L, 224744871L)); } - + @Test - void createStatistics_with_odd_number_of_values(){ + void createStatistics_with_odd_number_of_values() { Statistics statistics = Durations.createStatistics(Arrays.asList( Duration.ofSeconds(1), Duration.ofSeconds(2), Duration.ofSeconds(4) )); - + assertThat(statistics).isNotNull(); - assertThat(statistics.getSum()).isEqualTo(Duration.ofSeconds(7)); - assertThat(statistics.getMean()).isEqualTo(Duration.parse("PT2.333333333S")); - assertThat(statistics.getMoe95()).isEqualTo(Duration.parse("PT1.440164599S")); + assertThat(statistics.getSum()).isEqualTo(createDuration(7L, 0L)); + assertThat(statistics.getMean()).isEqualTo(createDuration(2L, 333333333L)); + assertThat(statistics.getMoe95()).isEqualTo(createDuration(1L, 440164599L)); + } + + private static io.cucumber.messages.types.Duration createDuration(long seconds, long nanos) { + return new io.cucumber.messages.types.Duration(seconds, nanos); } } diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java index c9bdf95..7f7e53d 100644 --- a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java @@ -52,6 +52,7 @@ static List acceptance() { private static List getSources() { return Arrays.asList( + Paths.get("../testdata/src/ambiguous.ndjson"), Paths.get("../testdata/src/minimal.ndjson"), Paths.get("../testdata/src/unused-steps.ndjson"), Paths.get("../testdata/src/multiple-features.ndjson") diff --git a/testdata/src/ambiguous.json b/testdata/src/ambiguous.json new file mode 100644 index 0000000..03672bb --- /dev/null +++ b/testdata/src/ambiguous.json @@ -0,0 +1,30 @@ +{ + "stepDefinitions": [ + { + "sourceReference": { + "uri": "samples/ambiguous/ambiguous.ts", + "location": { + "line": 3 + } + }, + "matches": [ ], + "expression": { + "source": "^a (.*?) with (.*?)$", + "type": "REGULAR_EXPRESSION" + } + }, + { + "sourceReference": { + "uri": "samples/ambiguous/ambiguous.ts", + "location": { + "line": 7 + } + }, + "matches": [ ], + "expression": { + "source": "^a step with (.*)$", + "type": "REGULAR_EXPRESSION" + } + } + ] +} \ No newline at end of file diff --git a/testdata/src/ambiguous.step-definitions.txt b/testdata/src/ambiguous.step-definitions.txt new file mode 100644 index 0000000..e4552f7 --- /dev/null +++ b/testdata/src/ambiguous.step-definitions.txt @@ -0,0 +1,4 @@ + +Expression/Text Duration Mean ± Error Location +^a (.*?) with (.*?)$ samples/ambiguous/ambiguous.ts:3 +^a step with (.*)$ samples/ambiguous/ambiguous.ts:7 diff --git a/testdata/src/ambiguous.unused.txt b/testdata/src/ambiguous.unused.txt new file mode 100644 index 0000000..afda58c --- /dev/null +++ b/testdata/src/ambiguous.unused.txt @@ -0,0 +1,6 @@ + +2 unused step definition(s) + +Location Expression +samples/ambiguous/ambiguous.ts:3 # ^a (.*?) with (.*?)$ +samples/ambiguous/ambiguous.ts:7 # ^a step with (.*)$ diff --git a/testdata/src/ambiguous.with-steps.txt b/testdata/src/ambiguous.with-steps.txt new file mode 100644 index 0000000..28b18a8 --- /dev/null +++ b/testdata/src/ambiguous.with-steps.txt @@ -0,0 +1,6 @@ + +Expression/Text Duration Mean ± Error Location +^a (.*?) with (.*?)$ samples/ambiguous/ambiguous.ts:3 + UNUSED +^a step with (.*)$ samples/ambiguous/ambiguous.ts:7 + UNUSED diff --git a/testdata/src/minimal.json b/testdata/src/minimal.json index 3926c21..51c1555 100644 --- a/testdata/src/minimal.json +++ b/testdata/src/minimal.json @@ -1,20 +1,44 @@ { "stepDefinitions": [ { - "expression": "I have {int} cukes in my belly", - "location": "samples/minimal/minimal.ts:3", + "sourceReference": { + "uri": "samples/minimal/minimal.ts", + "location": { + "line": 3 + } + }, "duration": { - "sum": 0.001000000, - "mean": 0.001000000, - "moe95": 0.0 + "sum": { + "seconds": 0, + "nanos": 1000000 + }, + "mean": { + "seconds": 0, + "nanos": 1000000 + }, + "moe95": { + "seconds": 0, + "nanos": 0 + } }, - "steps": [ + "matches": [ { "text": "I have 42 cukes in my belly", - "duration": 0.001000000, - "location": "samples/minimal/minimal.feature:9" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/minimal/minimal.feature", + "location": { + "line": 9, + "column": 3 + } } - ] + ], + "expression": { + "source": "I have {int} cukes in my belly", + "type": "CUCUMBER_EXPRESSION" + } } ] } \ No newline at end of file diff --git a/testdata/src/multiple-features.json b/testdata/src/multiple-features.json index 3b6d56a..c60370e 100644 --- a/testdata/src/multiple-features.json +++ b/testdata/src/multiple-features.json @@ -1,60 +1,140 @@ { "stepDefinitions": [ { - "expression": "an order for {string}", - "location": "samples/multiple-features/multiple-features.ts:3", + "sourceReference": { + "uri": "samples/multiple-features/multiple-features.ts", + "location": { + "line": 3 + } + }, "duration": { - "sum": 0.009000000, - "mean": 0.001000000, - "moe95": 0.0 + "sum": { + "seconds": 0, + "nanos": 9000000 + }, + "mean": { + "seconds": 0, + "nanos": 1000000 + }, + "moe95": { + "seconds": 0, + "nanos": 0 + } }, - "steps": [ + "matches": [ { "text": "an order for \"eggs\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-1.feature:3" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-1.feature", + "location": { + "line": 3, + "column": 3 + } }, { "text": "an order for \"milk\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-1.feature:6" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-1.feature", + "location": { + "line": 6, + "column": 3 + } }, { "text": "an order for \"bread\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-1.feature:9" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-1.feature", + "location": { + "line": 9, + "column": 3 + } }, { "text": "an order for \"batteries\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-2.feature:3" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-2.feature", + "location": { + "line": 3, + "column": 3 + } }, { "text": "an order for \"light bulbs\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-2.feature:6" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-2.feature", + "location": { + "line": 6, + "column": 3 + } }, { "text": "an order for \"fuses\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-2.feature:9" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-2.feature", + "location": { + "line": 9, + "column": 3 + } }, { "text": "an order for \"pencils\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-3.feature:3" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-3.feature", + "location": { + "line": 3, + "column": 3 + } }, { "text": "an order for \"rulers\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-3.feature:6" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-3.feature", + "location": { + "line": 6, + "column": 3 + } }, { "text": "an order for \"paperclips\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-3.feature:9" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-3.feature", + "location": { + "line": 9, + "column": 3 + } } - ] + ], + "expression": { + "source": "an order for {string}", + "type": "CUCUMBER_EXPRESSION" + } } ] } \ No newline at end of file diff --git a/testdata/src/unused-steps.json b/testdata/src/unused-steps.json index b179d3f..11bd1ec 100644 --- a/testdata/src/unused-steps.json +++ b/testdata/src/unused-steps.json @@ -1,25 +1,57 @@ { "stepDefinitions": [ { - "expression": "a step that is used", - "location": "samples/unused-steps/unused-steps.ts:3", + "sourceReference": { + "uri": "samples/unused-steps/unused-steps.ts", + "location": { + "line": 3 + } + }, "duration": { - "sum": 0.001000000, - "mean": 0.001000000, - "moe95": 0.0 + "sum": { + "seconds": 0, + "nanos": 1000000 + }, + "mean": { + "seconds": 0, + "nanos": 1000000 + }, + "moe95": { + "seconds": 0, + "nanos": 0 + } }, - "steps": [ + "matches": [ { "text": "a step that is used", - "duration": 0.001000000, - "location": "samples/unused-steps/unused-steps.feature:5" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/unused-steps/unused-steps.feature", + "location": { + "line": 5, + "column": 3 + } } - ] + ], + "expression": { + "source": "a step that is used", + "type": "CUCUMBER_EXPRESSION" + } }, { - "expression": "a step that is not used", - "location": "samples/unused-steps/unused-steps.ts:7", - "steps": [ ] + "sourceReference": { + "uri": "samples/unused-steps/unused-steps.ts", + "location": { + "line": 7 + } + }, + "matches": [ ], + "expression": { + "source": "a step that is not used", + "type": "CUCUMBER_EXPRESSION" + } } ] } \ No newline at end of file From 2616a7a6642ce9b13ffabd73e44c180180930ebd Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 14 Oct 2025 03:34:34 +0200 Subject: [PATCH 11/13] Update README --- README.md | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cd92e2b..1eae6cb 100644 --- a/README.md +++ b/README.md @@ -28,18 +28,38 @@ The output can also be rendered as a json report. { "stepDefinitions": [ { - "expression": "an order for {string}", - "location": "samples/multiple-features/multiple-features.ts:3", + "sourceReference": { + "uri": "samples/multiple-features/multiple-features.ts", + "location": { + "line": 3 + } + }, "duration": { - "sum": 0.009000000, - "mean": 0.001000000, - "moe95": 0.0 + "sum": { + "seconds": 0, + "nanos": 9000000 + }, + "mean": { + "seconds": 0, + "nanos": 1000000 + }, + "moe95": { + "seconds": 0, + "nanos": 0 + } }, - "steps": [ + "matches": [ { "text": "an order for \"eggs\"", - "duration": 0.001000000, - "location": "samples/multiple-features/multiple-features-1.feature:3" + "duration": { + "seconds": 0, + "nanos": 1000000 + }, + "uri": "samples/multiple-features/multiple-features-1.feature", + "location": { + "line": 3, + "column": 3 + } }, ... ``` From ae25ba3cae469a105a05330a307893e980fa5ab7 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 14 Oct 2025 03:37:06 +0200 Subject: [PATCH 12/13] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1eae6cb..387399f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ The output can also be rendered as a json report. "nanos": 0 } }, + "expression": { + "source": "an order for {string}", + "type": "CUCUMBER_EXPRESSION" + }, "matches": [ { "text": "an order for \"eggs\"", From 7169a0af7d37cc88e5543bd0bfb690f885f536fe Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 14 Oct 2025 16:14:01 +0200 Subject: [PATCH 13/13] Drop unused dependency --- java/pom.xml | 9 ++------- .../test/java/io/cucumber/usageformatter/Jackson.java | 2 -- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index 7ac1fa6..69f2d12 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -79,12 +80,6 @@ test - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - test - - com.fasterxml.jackson.module jackson-module-parameter-names diff --git a/java/src/test/java/io/cucumber/usageformatter/Jackson.java b/java/src/test/java/io/cucumber/usageformatter/Jackson.java index 1aa12d1..da73dcb 100644 --- a/java/src/test/java/io/cucumber/usageformatter/Jackson.java +++ b/java/src/test/java/io/cucumber/usageformatter/Jackson.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.cfg.ConstructorDetector; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT; @@ -21,7 +20,6 @@ final class Jackson { public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() .addModule(new Jdk8Module()) - .addModule(new JavaTimeModule()) .addModule(new ParameterNamesModule(Mode.PROPERTIES)) .defaultPropertyInclusion(construct(NON_ABSENT, NON_ABSENT)) .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)