Skip to content

Commit

Permalink
Include pickle name if parameterized (#44)
Browse files Browse the repository at this point in the history
Cucumber originally only had Features with Scenarios. When Scenario
Outlines were added, it became hard to distinguish which example failed.
To work around this Scenario Outline may have placeholders in their
name. These placeholders are replaced when creating a pickle name. This
helps ensure that each scenario is unique.

```
Feature: Examples Tables

  Scenario Outline: Eating <eat> cucumbers
    Given there are <start> cucumbers
    When I eat <eat> cucumbers
    Then I should have <left> cucumbers

    Examples: These are passing
      | start | eat | left |
      |    12 |   5 |    7 |
```

This would be rendered as:

```
Examples Tables
  └── Eating 5 cucumbers
```

But with the addition of the Rule element and test frameworks supporting
hierarchical test structures it also becomes desirable to include the
structure of a feature file in the test name. So we would now
render:

```
Examples Tables
└── Eating cucumbers
    └──These are passing
        └── #1.1
```

And while this hierarchy is sufficient to identify a failed example, it
is not very easy to do so when there are many. So by including the
pickle name when the Scenario Outline is parameterized we can render:

```
Examples Tables
└── Eating <eat> cucumbers
    └──These are passing
        └── #1.1: Eating 5 cucumbers
```
  • Loading branch information
mpkorstanje authored Jun 22, 2024
1 parent 2f6f474 commit f37e42b
Show file tree
Hide file tree
Showing 14 changed files with 921 additions and 332 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Changed
- Include pickle name if parameterized ((#44)[https://github.com/cucumber/query/pull/44])

### Fixed
- java: Require all arguments to the naming strategy builder to be non-null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,62 @@

import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

/**
* A structure containing all ancestors of a given element.
* A structure containing all ancestors of a given
* {@linkplain GherkinDocument GherkinDocument element} or
* {@link io.cucumber.messages.types.Pickle}.
* <p>
* This works without any ordering because Gherkins document
* structure is simple enough to hard code.
*
* @see LineageReducer
*/
class GherkinDocumentElements {
class Lineage {

private final GherkinDocument document;
private final Feature feature;
private final Rule rule;
private final Scenario scenario;
private final Examples examples;
private final TableRow example;
private final Integer examplesIndex;
private final TableRow example;
private final Integer exampleIndex;

GherkinDocumentElements(GherkinDocument document, Feature feature, Rule rule, Scenario scenario) {
this(document, feature, rule, scenario, null, null, null, null);
Lineage(GherkinDocument document) {
this(document, null, null, null, null, null, null, null);
}

Lineage(Lineage parent, Feature feature) {
this(parent.document, feature, null, null, null, null, null, null);
}

GherkinDocumentElements(GherkinDocument document, Feature feature, Rule rule, Scenario scenario, Integer examplesIndex, Examples examples, Integer exampleIndex, TableRow example) {
Lineage(Lineage parent, Rule rule) {
this(parent.document, parent.feature, rule, null, null, null, null, null);
}

Lineage(Lineage parent, Scenario scenario) {
this(parent.document, parent.feature, parent.rule, scenario, null, null, null, null);
}

Lineage(Lineage parent, Examples examples, int examplesIndex) {
this(parent.document, parent.feature, parent.rule, parent.scenario, examples, examplesIndex, null, null);
}

Lineage(Lineage parent, TableRow example, int exampleIndex) {
this(parent.document, parent.feature, parent.rule, parent.scenario, parent.examples, parent.examplesIndex, example, exampleIndex);
}

private Lineage(GherkinDocument document, Feature feature, Rule rule, Scenario scenario, Examples examples, Integer examplesIndex, TableRow example, Integer exampleIndex) {
this.document = requireNonNull(document);
this.feature = feature;
this.rule = rule;
this.scenario = scenario;
this.examplesIndex = examplesIndex;
this.examples = examples;
this.exampleIndex = exampleIndex;
this.examplesIndex = examplesIndex;
this.example = example;
this.exampleIndex = exampleIndex;
}

GherkinDocument document() {
Expand Down Expand Up @@ -75,11 +99,15 @@ Optional<Integer> exampleIndex() {
return Optional.ofNullable(exampleIndex);
}

<T> LineageReducer reduce(Supplier<LineageCollector<T>> collector) {
return new LineageReducerDescending(collector);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GherkinDocumentElements that = (GherkinDocumentElements) o;
Lineage that = (Lineage) o;
return document.equals(that.document) && feature.equals(that.feature) && Objects.equals(rule, that.rule) && scenario.equals(that.scenario) && Objects.equals(examples, that.examples) && Objects.equals(example, that.example) && Objects.equals(examplesIndex, that.examplesIndex) && Objects.equals(exampleIndex, that.exampleIndex);
}

Expand Down
45 changes: 45 additions & 0 deletions java/src/main/java/io/cucumber/query/LineageCollector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.cucumber.query;

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;

/**
* Collect the {@link Lineage} of a
* {@linkplain io.cucumber.messages.types.GherkinDocument GherkinDocument element}
* or {@link Pickle} and reduce it to a single result.
*
* @param <T> the type reduced to.
*/
interface LineageCollector<T> {
default void add(GherkinDocument document) {

}

default void add(Feature feature) {

}

default void add(Rule rule) {

}

default void add(Scenario scenario) {

}

default void add(Examples examples, int index) {
}

default void add(TableRow example, int index) {
}

default void add(Pickle pickle) {
}

T finish();
}
30 changes: 30 additions & 0 deletions java/src/main/java/io/cucumber/query/LineageReducer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.cucumber.query;

import io.cucumber.messages.types.Pickle;

import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

/**
* Visit the {@link Lineage} of a {@linkplain io.cucumber.messages.types.GherkinDocument GherkinDocument element}
* or {@link Pickle} and reduce it.
* <p>
* Because we are using messages we can not express the hierarchy of elements in
* a {@link io.cucumber.messages.types.GherkinDocument} programmatically as a
* tree of nodes. But we can still express the operations that would be typically
* done this way as an operation on the lineage of those messages.
*
* @param <T> the type reduced to.
*/
interface LineageReducer<T> {

static <T> LineageReducer<T> descending(Supplier<? extends LineageCollector<T>> collector) {
return new LineageReducerDescending<>(collector);
}

T reduce(Lineage lineage);

T reduce(Lineage lineage, Pickle pickle);

}
45 changes: 45 additions & 0 deletions java/src/main/java/io/cucumber/query/LineageReducerDescending.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.cucumber.query;

import io.cucumber.messages.types.Pickle;

import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

/**
* Reduces the lineage of a Gherkin document element in descending order.
*
* @param <T> type to which the lineage is reduced.
*/
class LineageReducerDescending<T> implements LineageReducer<T> {

private final Supplier<? extends LineageCollector<T>> reducerSupplier;

LineageReducerDescending(Supplier<? extends LineageCollector<T>> reducerSupplier) {
this.reducerSupplier = requireNonNull(reducerSupplier);
}

@Override
public T reduce(Lineage lineage) {
LineageCollector<T> reducer = reducerSupplier.get();
reduceAddLineage(reducer, lineage);
return reducer.finish();
}

@Override
public T reduce(Lineage lineage, Pickle pickle) {
LineageCollector<T> reducer = reducerSupplier.get();
reduceAddLineage(reducer, lineage);
reducer.add(pickle);
return reducer.finish();
}

private static <T> void reduceAddLineage(LineageCollector<T> reducer, Lineage lineage) {
reducer.add(lineage.document());
lineage.feature().ifPresent(reducer::add);
lineage.rule().ifPresent(reducer::add);
lineage.scenario().ifPresent(reducer::add);
lineage.examples().ifPresent(examples -> reducer.add(examples, lineage.examplesIndex().orElse(0)));
lineage.example().ifPresent(example -> reducer.add(example, lineage.exampleIndex().orElse(0)));
}
}
92 changes: 92 additions & 0 deletions java/src/main/java/io/cucumber/query/Lineages.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.cucumber.query;

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;

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 {

/**
* Create map of a {@link GherkinDocument} element to its {@link Lineage} in that document.
* <p>
* @param document to create the lineage of
* @return a map of the document elements to their lineage.
*/
static Map<String, Lineage> of(GherkinDocument document) {
Map<String, Lineage> elements = new HashMap<>();
Lineage lineage = new Lineage(document);
String uri = document.getUri()
.orElseThrow(() -> new IllegalArgumentException("document.uri must not be null"));
elements.put(uri, lineage);
document.getFeature().ifPresent(ofFeature(lineage, elements));
return elements;
}

private static Consumer<Feature> ofFeature(Lineage parent, Map<String, Lineage> elements) {
return feature -> {
Lineage lineage = new Lineage(parent, feature);
feature.getChildren().forEach(ofFeatureChild(lineage, elements));
};
}

private static Consumer<FeatureChild> ofFeatureChild(Lineage parent, Map<String, Lineage> elements) {
return featureChild -> {
featureChild.getScenario().ifPresent(ofScenario(parent, elements));
featureChild.getRule().ifPresent(ofRule(parent, elements));
};
}

private static Consumer<Rule> ofRule(Lineage parent, Map<String, Lineage> elements) {
return rule -> {
Lineage lineage = new Lineage(parent, rule);
elements.put(rule.getId(), lineage);
rule.getChildren().forEach(ofRuleChild(lineage, elements));
};
}

private static Consumer<RuleChild> ofRuleChild(Lineage parent, Map<String, Lineage> elements) {
return ruleChild -> ruleChild.getScenario().ifPresent(ofScenario(parent, elements));
}

private static Consumer<Scenario> ofScenario(Lineage parent, Map<String, Lineage> elements) {
return scenario -> {
Lineage lineage = new Lineage(parent, scenario);
elements.put(scenario.getId(), lineage);
forEachIndexed(scenario.getExamples(), ofExamples(lineage, elements));
};
}

private static BiConsumer<Examples, Integer> ofExamples(Lineage parent, Map<String, Lineage> elements) {
return (examples, examplesIndex) -> {
Lineage lineage = new Lineage(parent, examples, examplesIndex);
elements.put(examples.getId(), lineage);
forEachIndexed(examples.getTableBody(), ofExample(lineage, elements));
};
}

private static BiConsumer<TableRow, Integer> ofExample(Lineage parent, Map<String, Lineage> elements) {
return (example, exampleIndex) -> {
Lineage lineage = new Lineage(parent, example, exampleIndex);
elements.put(example.getId(), lineage);
};
}

private static <T> void forEachIndexed(List<T> items, BiConsumer<T, Integer> consumer) {
for (int i = 0; i < items.size(); i++) {
consumer.accept(items.get(i), i);
}
}
}
Loading

0 comments on commit f37e42b

Please sign in to comment.