diff --git a/CHANGELOG.md b/CHANGELOG.md index ac58b4d4..38f90ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- [Java, JavaScript] Add `Lineage.background()` and `.ruleBackground()` ([#140](https://github.com/cucumber/query/pull/140)) ### Removed - [JavaScript] BREAKING CHANGE: Remove defunct legacy methods from `Query` ([#141](https://github.com/cucumber/query/pull/141)) diff --git a/java/src/main/java/io/cucumber/query/Lineage.java b/java/src/main/java/io/cucumber/query/Lineage.java index 37b1e8c3..a1d80285 100644 --- a/java/src/main/java/io/cucumber/query/Lineage.java +++ b/java/src/main/java/io/cucumber/query/Lineage.java @@ -1,9 +1,12 @@ package io.cucumber.query; +import io.cucumber.messages.types.Background; import io.cucumber.messages.types.Examples; import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.FeatureChild; import io.cucumber.messages.types.GherkinDocument; import io.cucumber.messages.types.Rule; +import io.cucumber.messages.types.RuleChild; import io.cucumber.messages.types.Scenario; import io.cucumber.messages.types.TableRow; @@ -74,10 +77,32 @@ public Optional feature() { return Optional.ofNullable(feature); } + public Optional background() { + if (feature == null) { + return Optional.empty(); + } + return feature.getChildren().stream() + .map(FeatureChild::getBackground) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + public Optional rule() { return Optional.ofNullable(rule); } + public Optional ruleBackground() { + if (rule == null) { + return Optional.empty(); + } + return rule.getChildren().stream() + .map(RuleChild::getBackground) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + public Optional scenario() { return Optional.ofNullable(scenario); } diff --git a/java/src/main/java/io/cucumber/query/Lineages.java b/java/src/main/java/io/cucumber/query/Lineages.java index a82b0bc1..0deec269 100644 --- a/java/src/main/java/io/cucumber/query/Lineages.java +++ b/java/src/main/java/io/cucumber/query/Lineages.java @@ -12,10 +12,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.function.Supplier; class Lineages { diff --git a/java/src/test/java/io/cucumber/query/LineageTest.java b/java/src/test/java/io/cucumber/query/LineageTest.java new file mode 100644 index 00000000..498e6881 --- /dev/null +++ b/java/src/test/java/io/cucumber/query/LineageTest.java @@ -0,0 +1,128 @@ +package io.cucumber.query; + +import io.cucumber.messages.NdjsonToMessageIterable; +import io.cucumber.messages.types.Background; +import io.cucumber.messages.types.Envelope; +import io.cucumber.messages.types.Examples; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.GherkinDocument; +import io.cucumber.messages.types.Pickle; +import io.cucumber.messages.types.Rule; +import io.cucumber.messages.types.Scenario; +import io.cucumber.messages.types.TableRow; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static io.cucumber.query.Repository.RepositoryFeature.INCLUDE_GHERKIN_DOCUMENTS; +import static org.assertj.core.api.Assertions.assertThat; + +class LineageTest { + + final Repository repository = Repository.builder() + .feature(INCLUDE_GHERKIN_DOCUMENTS, true) + .build(); + final Query query = new Query(repository); + + @Test + void minimal() throws IOException { + List messages = readMessages(Paths.get("../testdata/src/minimal.ndjson")); + messages.forEach(repository::update); + Pickle pickle = query.findAllPickles().stream() + .findFirst() + .get(); + Lineage lineage = query.findLineageBy(pickle).get(); + + GherkinDocument gherkinDocument = messages.stream().filter(envelope -> envelope.getGherkinDocument().isPresent()) + .map(Envelope::getGherkinDocument) + .map(Optional::get) + .findFirst() + .get(); + Optional feature = gherkinDocument.getFeature(); + Optional scenario = feature.get().getChildren().get(0).getScenario(); + + assertThat(lineage.document()).isEqualTo(gherkinDocument); + assertThat(lineage.feature()).isEqualTo(feature); + assertThat(lineage.background()).isEmpty(); + assertThat(lineage.rule()).isEmpty(); + assertThat(lineage.ruleBackground()).isEmpty(); + assertThat(lineage.scenario()).isEqualTo(scenario); + assertThat(lineage.examples()).isEmpty(); + assertThat(lineage.example()).isEmpty(); + } + + @Test + void exampleTables() throws IOException { + List messages = readMessages(Paths.get("../testdata/src/examples-tables.ndjson")); + messages.forEach(repository::update); + Pickle pickle = query.findAllPickles().stream() + .findFirst() + .get(); + Lineage lineage = query.findLineageBy(pickle).get(); + + GherkinDocument gherkinDocument = messages.stream().filter(envelope -> envelope.getGherkinDocument().isPresent()) + .map(Envelope::getGherkinDocument) + .map(Optional::get) + .findFirst() + .get(); + Optional feature = gherkinDocument.getFeature(); + Optional scenario = feature.get().getChildren().get(0).getScenario(); + Examples examples = scenario.get().getExamples().get(0); + TableRow example = examples.getTableBody().get(0); + + assertThat(lineage.document()).isEqualTo(gherkinDocument); + assertThat(lineage.feature()).isEqualTo(feature); + assertThat(lineage.background()).isEmpty(); + assertThat(lineage.rule()).isEmpty(); + assertThat(lineage.ruleBackground()).isEmpty(); + assertThat(lineage.scenario()).isEqualTo(scenario); + assertThat(lineage.examples()).contains(examples); + assertThat(lineage.example()).contains(example); + } + + @Test + void rulesBackgrounds() throws IOException { + List messages = readMessages(Paths.get("../testdata/src/rules-backgrounds.ndjson")); + messages.forEach(repository::update); + Pickle pickle = query.findAllPickles().stream() + .findFirst() + .get(); + Lineage lineage = query.findLineageBy(pickle).get(); + + GherkinDocument gherkinDocument = messages.stream().filter(envelope -> envelope.getGherkinDocument().isPresent()) + .map(Envelope::getGherkinDocument) + .map(Optional::get) + .findFirst() + .get(); + Optional feature = gherkinDocument.getFeature(); + Optional background = feature.get().getChildren().get(0).getBackground(); + Optional rule = feature.get().getChildren().get(1).getRule(); + Optional ruleBackGround = rule.get().getChildren().get(0).getBackground(); + Optional scenario = rule.get().getChildren().get(1).getScenario(); + + assertThat(lineage.document()).isEqualTo(gherkinDocument); + assertThat(lineage.feature()).isEqualTo(feature); + assertThat(lineage.background()).isEqualTo(background); + assertThat(lineage.rule()).isEqualTo(rule); + assertThat(lineage.ruleBackground()).isEqualTo(ruleBackGround); + assertThat(lineage.scenario()).isEqualTo(scenario); + assertThat(lineage.examples()).isEmpty(); + assertThat(lineage.example()).isEmpty(); + } + + private static @NonNull List readMessages(Path path) throws IOException { + InputStream in = Files.newInputStream(path); + NdjsonToMessageIterable messages = new NdjsonToMessageIterable(in, json -> Jackson.OBJECT_MAPPER.readValue(json, Envelope.class)); + List e = new ArrayList<>(); + messages.forEach(e::add); + return e; + } +} diff --git a/javascript/src/Lineage.ts b/javascript/src/Lineage.ts index a5214253..bc6b6eb5 100644 --- a/javascript/src/Lineage.ts +++ b/javascript/src/Lineage.ts @@ -1,4 +1,5 @@ import { + Background, Examples, Feature, GherkinDocument, @@ -11,7 +12,9 @@ import { export interface Lineage { gherkinDocument?: GherkinDocument feature?: Feature + background?: Background rule?: Rule + ruleBackground?: Background scenario?: Scenario examples?: Examples examplesIndex?: number diff --git a/javascript/src/Query.spec.ts b/javascript/src/Query.spec.ts index ebfb9353..9a2ebe40 100644 --- a/javascript/src/Query.spec.ts +++ b/javascript/src/Query.spec.ts @@ -1,7 +1,10 @@ import assert from 'node:assert' +import fs from 'node:fs/promises' +import path from 'node:path' -import { TestCaseStarted } from '@cucumber/messages' +import { Envelope, TestCaseStarted } from '@cucumber/messages' +import { Lineage } from './Lineage' import Query from './Query' describe('Query', () => { @@ -84,4 +87,89 @@ describe('Query', () => { assert.deepStrictEqual(cucumberQuery.findAllTestCaseStarted(), testCasesStarted) }) }) + + describe('#findLineageBy', () => { + it('returns correct lineage for a minimal scenario', async () => { + const envelopes: ReadonlyArray = ( + await fs.readFile(path.join(__dirname, '../../testdata/src/minimal.ndjson'), { + encoding: 'utf-8', + }) + ) + .split('\n') + .filter((line) => !!line) + .map((line) => JSON.parse(line)) + envelopes.forEach((envelope) => cucumberQuery.update(envelope)) + + const gherkinDocument = envelopes.find((envelope) => envelope.gherkinDocument).gherkinDocument + const feature = gherkinDocument.feature + const scenario = feature.children.find((child) => child.scenario).scenario + const pickle = envelopes.find((envelope) => envelope.pickle).pickle + + assert.deepStrictEqual(cucumberQuery.findLineageBy(pickle), { + gherkinDocument, + feature, + scenario, + } satisfies Lineage) + }) + + it('returns correct lineage for a pickle from an examples table', async () => { + const envelopes: ReadonlyArray = ( + await fs.readFile(path.join(__dirname, '../../testdata/src/examples-tables.ndjson'), { + encoding: 'utf-8', + }) + ) + .split('\n') + .filter((line) => !!line) + .map((line) => JSON.parse(line)) + envelopes.forEach((envelope) => cucumberQuery.update(envelope)) + + const gherkinDocument = envelopes.find((envelope) => envelope.gherkinDocument).gherkinDocument + const feature = gherkinDocument.feature + const scenario = feature.children.find((child) => child.scenario).scenario + const pickle = envelopes.find((envelope) => envelope.pickle).pickle + const examples = scenario.examples[0] + const example = examples.tableBody[0] + + assert.deepStrictEqual(cucumberQuery.findLineageBy(pickle), { + gherkinDocument, + feature, + scenario, + examples, + examplesIndex: 0, + example, + exampleIndex: 0, + } satisfies Lineage) + }) + + it('returns correct lineage for a pickle with background-derived steps', async () => { + const envelopes: ReadonlyArray = ( + await fs.readFile(path.join(__dirname, '../../testdata/src/rules-backgrounds.ndjson'), { + encoding: 'utf-8', + }) + ) + .split('\n') + .filter((line) => !!line) + .map((line) => JSON.parse(line)) + envelopes.forEach((envelope) => cucumberQuery.update(envelope)) + + const gherkinDocument = envelopes.find((envelope) => envelope.gherkinDocument).gherkinDocument + const feature = gherkinDocument.feature + const background = gherkinDocument.feature.children.find( + (child) => child.background + ).background + const rule = feature.children.find((child) => child.rule).rule + const ruleBackground = rule.children.find((child) => child.background).background + const scenario = rule.children.find((child) => child.scenario).scenario + const pickle = envelopes.find((envelope) => envelope.pickle).pickle + + assert.deepStrictEqual(cucumberQuery.findLineageBy(pickle), { + gherkinDocument, + feature, + background, + rule, + ruleBackground, + scenario, + } satisfies Lineage) + }) + }) }) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index c0e71ac6..619d7c99 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -129,6 +129,7 @@ export default class Query { private updateFeature(feature: Feature, lineage: Lineage) { feature.children.forEach((featureChild) => { if (featureChild.background) { + lineage.background = featureChild.background this.updateSteps(featureChild.background.steps) } if (featureChild.scenario) { @@ -149,6 +150,7 @@ export default class Query { private updateRule(rule: Rule, lineage: Lineage) { rule.children.forEach((ruleChild) => { if (ruleChild.background) { + lineage.ruleBackground = ruleChild.background this.updateSteps(ruleChild.background.steps) } if (ruleChild.scenario) {