Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add afterScenarioOutline & karate.scenarioOutline #2636

Merged
merged 4 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3690,6 +3690,7 @@ Operation | Description
<a name="karate-response"><code>karate.response</code></a> | returns the last HTTP response as a JS object that enables advanced use-cases such as getting a header ignoring case: `karate.response.header('some-header')`
<a name="karate-request"><code>karate.request</code></a> | returns the last HTTP request as a JS object that enables advanced use-cases such as getting a header ignoring case: `karate.request.header('some-header')`, which works [even in mocks](https://github.com/karatelabs/karate/tree/master/karate-netty#requestheaders)
<a name="karate-scenario"><code>karate.scenario</code></a> | get metadata about the currently executing `Scenario` (or `Outline` - `Example`) within a test
<a name="karate-scenarioOutline"><code>karate.scenarioOutline</code></a> | get metadata about the currently executing scenario outline within a test
<a name="karate-set"><code>karate.set(name, value)</code></a> | sets the value of a variable (immediately), which may be needed in case any other routines (such as the [configured headers](#configure-headers)) depend on that variable
<a name="karate-setall"><code>karate.set(object)</code></a> | where the single argument is expected to be a `Map` or JSON-like, and will perform the above `karate.set()` operation for all key-value pairs in one-shot
<a name="karate-setpath"><code>karate.set(name, path, value)</code></a> | only needed when you need to conditionally build payload elements, especially XML. This is best explained via [an example](karate-core/src/test/java/com/intuit/karate/core/xml/xml.feature#L211), and it behaves the same way as the [`set`](#set) keyword. Also see [`eval`](#eval).
Expand Down Expand Up @@ -4530,7 +4531,7 @@ This is great for testing boundary conditions against a single end-point, with t
### Scenario Outline Enhancements
Karate has enhanced the Cucumber `Scenario Outline` as follows:
* __Type Hints__: if the `Examples` column header has a `!` appended, each value will be evaluated as a JavaScript data-type (number, boolean, or *even* in-line JSON) - else it defaults to string.
* __Magic Variables__: `__row` gives you the entire row as a JSON object, and `__num` gives you the row index (the first row is `0`).
* __Magic Variables__: `__row` gives you the entire row as a JSON object, and `__num` gives you the row index (the first row is `0`). In rare cases you may have multiple `Examples` tables for a single outline, `__tableNum` will give you the index of the currently executing table.
* __Auto Variables__: in addition to `__row`, each column key-value will be available as a separate [variable](#def), which greatly simplifies JSON manipulation - especially when you want to re-use JSON [files](#reading-files) containing [embedded expressions](#embedded-expressions).
* Any empty cells will result in a `null` value for that column-key, and this can be useful to [remove nodes](#remove-if-null) from JSON or XML documents

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
package com.intuit.karate.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
*
Expand All @@ -34,11 +36,14 @@ public class ExamplesTable {

private final ScenarioOutline outline;
private final Table table;
private final int index;
private List<Tag> tags;


public ExamplesTable(ScenarioOutline outline, Table table) {
public ExamplesTable(ScenarioOutline outline, Table table, int index) {
this.outline = outline;
this.table = table;
this.index = index;
this.tags = new ArrayList();
}

Expand All @@ -58,4 +63,17 @@ public Table getTable() {
return table;
}

public int getIndex() {
return index;
}

public Map<String, Object> toKarateJson() {
Map<String, Object> map = new HashMap();
List<String> tagStrings = new ArrayList();
tags.forEach(tag -> tagStrings.add(tag.toString()));
map.put("tags", tagStrings);
map.put("data", table.getRowsAsMapsConverted());
return map;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public void enterBackground(KarateParser.BackgroundContext ctx) {
@Override
public void enterScenario(KarateParser.ScenarioContext ctx) {
FeatureSection section = new FeatureSection();
Scenario scenario = new Scenario(feature, section, -1);
Scenario scenario = new Scenario(feature, section, -1, -1);
scenario.setLine(getActualLine(ctx.SCENARIO()));
section.setScenario(scenario);
feature.addSection(section);
Expand Down Expand Up @@ -279,9 +279,10 @@ public void enterScenarioOutline(KarateParser.ScenarioOutlineContext ctx) {
}
List<ExamplesTable> examples = new ArrayList(ctx.examples().size());
outline.setExamplesTables(examples);
int tableIndex = 0;
for (KarateParser.ExamplesContext ec : ctx.examples()) {
Table table = toTable(ec.table());
ExamplesTable example = new ExamplesTable(outline, table);
ExamplesTable example = new ExamplesTable(outline, table, tableIndex++);
examples.add(example);
if (ec.tags() != null) {
example.setTags(toTags(-1, ec.tags().TAGS()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ private ScenarioRuntime initRuntime(Feature feature, Map<String, Object> args) {
FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(HttpClientFactory.DEFAULT), new FeatureCall(feature), args);
FeatureSection section = new FeatureSection();
section.setIndex(-1); // TODO util for creating dummy scenario
Scenario dummy = new Scenario(feature, section, -1);
Scenario dummy = new Scenario(feature, section, -1, -1);
section.setScenario(dummy);
ScenarioRuntime runtime = new ScenarioRuntime(featureRuntime, dummy);
runtime.logger.setLogOnly(true);
Expand Down
21 changes: 18 additions & 3 deletions karate-core/src/main/java/com/intuit/karate/core/Scenario.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class Scenario {

private final Feature feature;
private final FeatureSection section;
private final int exampleTableIndex;
private final int exampleIndex;

private int line;
Expand All @@ -46,9 +47,10 @@ public class Scenario {
private Map<String, Object> exampleData;
private String dynamicExpression;

public Scenario(Feature feature, FeatureSection section, int exampleIndex) {
public Scenario(Feature feature, FeatureSection section, int exampleTableIndex, int exampleIndex) {
this.feature = feature;
this.section = section;
this.exampleTableIndex = exampleTableIndex;
this.exampleIndex = exampleIndex;
}

Expand Down Expand Up @@ -80,7 +82,7 @@ public String getRefIdAndName() {

// only called for dynamic scenarios
public Scenario copy(int exampleIndex) {
Scenario s = new Scenario(feature, section, exampleIndex);
Scenario s = new Scenario(feature, section, exampleTableIndex, exampleIndex);
s.name = name;
s.description = description;
s.tags = tags;
Expand Down Expand Up @@ -134,6 +136,9 @@ public Step getStepByLine(int line) {
public String getRefId() {
int num = section.getIndex() + 1;
String meta = "[" + num;
if (exampleTableIndex != -1) {
meta = meta + "." + (exampleTableIndex + 1);
}
if (exampleIndex != -1) {
meta = meta + "." + (exampleIndex + 1);
}
Expand All @@ -146,7 +151,13 @@ public String getDebugInfo() {

public String getUniqueId() {
String id = feature.getResource().getPackageQualifiedName() + "_" + (section.getIndex() + 1);
return exampleIndex == -1 ? id : id + "_" + (exampleIndex + 1);
if (exampleTableIndex != -1) {
id += "_" + (exampleTableIndex + 1);
}
if (exampleIndex != -1) {
id += "_" + (exampleIndex + 1);
}
return id;
}

public List<Step> getBackgroundSteps() {
Expand Down Expand Up @@ -245,6 +256,10 @@ public void setExampleData(Map<String, Object> exampleData) {
this.exampleData = exampleData;
}

public int getExampleTableIndex() {
return exampleTableIndex;
}

public int getExampleIndex() {
return exampleIndex;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,10 @@ public Object getScenario() {
return new JsMap(getEngine().runtime.result.toKarateJson());
}

public Object getScenarioOutline() {
return new JsMap(getEngine().runtime.outlineResult.toKarateJson());
}

public Object getTags() {
return JsValue.fromJava(getEngine().runtime.tags.getTags());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* @author pthomas3
Expand All @@ -40,14 +41,15 @@ public class ScenarioOutline {
private String description;
private List<Step> steps;
private List<ExamplesTable> examplesTables;
private int numScenarios = 0;

public ScenarioOutline(Feature feature, FeatureSection section) {
this.feature = feature;
this.section = section;
}

public Scenario toScenario(String dynamicExpression, int exampleIndex, int updateLine, List<Tag> tagsForExamples) {
Scenario s = new Scenario(feature, section, exampleIndex);
public Scenario toScenario(String dynamicExpression, int exampleTableIndex, int exampleIndex, int updateLine, List<Tag> tagsForExamples) {
Scenario s = new Scenario(feature, section, exampleTableIndex, exampleIndex);
s.setName(name);
s.setDescription(description);
s.setLine(updateLine);
Expand Down Expand Up @@ -75,6 +77,7 @@ public Scenario toScenario(String dynamicExpression, int exampleIndex, int updat
step.setTable(original.getTable());
step.setComments(original.getComments());
}
numScenarios++;
return s;
}

Expand All @@ -101,13 +104,13 @@ public List<Scenario> getScenarios(FeatureRuntime fr) {
if (selectedForExecution) {
Table table = examples.getTable();
if (table.isDynamic()) {
Scenario scenario = toScenario(table.getDynamicExpression(), -1, table.getLineNumberForRow(0), examples.getTags());
Scenario scenario = toScenario(table.getDynamicExpression(), examples.getIndex(), -1, table.getLineNumberForRow(0), examples.getTags());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so following up from my previous comment - suggest since in this loop we know how many examples and example tables are there - we will be able to emit an extra boolean if this is the last scenario selected for execution within an outline

if that solves your current ask - I would just do that, as I really don't see a need for tracking counts of examples and tables :|

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good I can definitely make that simplification. What about the scenarioResults though? What do you feel would be the best way to get that information from an API perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I just removed the table index code in 6e84b5d. Didn't want to push too far ahead before we finalized something.

list.add(scenario);
} else {
int rowCount = table.getRows().size();
for (int i = 1; i < rowCount; i++) { // don't include header row
int exampleIndex = i - 1; // next line will set exampleIndex on scenario
Scenario scenario = toScenario(null, exampleIndex, table.getLineNumberForRow(i), examples.getTags());
Scenario scenario = toScenario(null, examples.getIndex(), exampleIndex, table.getLineNumberForRow(i), examples.getTags());
scenario.setExampleData(table.getExampleData(exampleIndex)); // and we set exampleData here
list.add(scenario);
for (String key : table.getKeys()) {
Expand Down Expand Up @@ -167,9 +170,23 @@ public void setSteps(List<Step> steps) {
public List<ExamplesTable> getExamplesTables() {
return examplesTables;
}

public int getNumExampleTables() {
return examplesTables.size();
}

public List<Map<String, Object>> getAllExampleData() {
List<Map<String, Object>> exampleData = new ArrayList();
examplesTables.forEach(table -> exampleData.add(table.toKarateJson()));
return exampleData;
}

public void setExamplesTables(List<ExamplesTable> examplesTables) {
this.examplesTables = examplesTables;
}

public int getNumScenarios() {
return numScenarios;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* The MIT License
*
* Copyright 2022 Karate Labs Inc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nts: update date

*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.intuit.karate.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
*
* @author OwenK2
*/
public class ScenarioOutlineResult {

final private ScenarioOutline scenarioOutline;
final private ScenarioRuntime runtime;

public ScenarioOutlineResult(ScenarioOutline scenarioOutline, ScenarioRuntime runtime) {
// NOTE: this value can be null, in which case the scenario is not from an outline
this.scenarioOutline = scenarioOutline;
this.runtime = runtime;
}

public Map<String, Object> toKarateJson() {
if (scenarioOutline == null) return null;
Map<String, Object> map = new HashMap();
map.put("name", scenarioOutline.getName());
map.put("description", scenarioOutline.getDescription());
map.put("line", scenarioOutline.getLine());
map.put("sectionIndex", scenarioOutline.getSection().getIndex());
map.put("exampleTableCount", scenarioOutline.getNumExampleTables());
map.put("exampleTables", scenarioOutline.getAllExampleData());
map.put("numScenariosToExecute", scenarioOutline.getNumScenarios());

// Get results of other examples in this outline
List<Map<String, Object>> scenarioResults = new ArrayList();
if (runtime.featureRuntime != null && runtime.featureRuntime.result != null) {
// Add all past results
runtime.featureRuntime.result.getScenarioResults().forEach(result -> {
if (result.getScenario().getSection().getIndex() == scenarioOutline.getSection().getIndex()) {
scenarioResults.add(result.toInfoJson());
}
});

// Add most recent result
if (runtime.result != null) {
scenarioResults.add(runtime.result.toInfoJson());
}
}
map.put("scenarioResults", scenarioResults);
map.put("numScenariosExecuted", scenarioResults.size());

return map;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public int compareTo(ScenarioResult sr) {
if (delta != 0) {
return delta;
}
delta = scenario.getExampleTableIndex() - sr.scenario.getExampleTableIndex();
if (delta != 0) {
return delta;
}
return scenario.getExampleIndex() - sr.scenario.getExampleIndex();
}

Expand Down Expand Up @@ -137,9 +141,10 @@ private List<Map> getStepResults(boolean background) {

public static ScenarioResult fromKarateJson(File workingDir, Feature feature, Map<String, Object> map) {
int sectionIndex = (Integer) map.get("sectionIndex");
int exampleTableIndex = (Integer) map.get("exampleTableIndex");
int exampleIndex = (Integer) map.get("exampleIndex");
FeatureSection section = feature.getSection(sectionIndex);
Scenario scenario = new Scenario(feature, section, exampleIndex);
Scenario scenario = new Scenario(feature, section, exampleTableIndex, exampleIndex);
if (section.isOutline()) {
scenario.setTags(section.getScenarioOutline().getTags());
scenario.setDescription(section.getScenarioOutline().getDescription());
Expand Down Expand Up @@ -194,6 +199,7 @@ public Map<String, Object> toKarateJson() {
}
//======================================================================
map.put("sectionIndex", scenario.getSection().getIndex());
map.put("exampleTableIndex", scenario.getExampleTableIndex());
map.put("exampleIndex", scenario.getExampleIndex());
Map<String, Object> exampleData = scenario.getExampleData();
if (exampleData != null) {
Expand All @@ -213,6 +219,28 @@ public Map<String, Object> toKarateJson() {
return map;
}

// Paired down information for use in karate.scenarioOutline
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nts: pared

public Map<String, Object> toInfoJson() {
Map<String, Object> map = new HashMap();
map.put("durationMillis", getDurationMillis());
List<String> tags = scenario.getTagsEffective().getTags();
if (tags != null && !tags.isEmpty()) {
map.put("tags", tags);
}
map.put("failed", isFailed());
map.put("refId", scenario.getRefId());
map.put("sectionIndex", scenario.getSection().getIndex());
map.put("exampleTableIndex", scenario.getExampleTableIndex());
map.put("exampleIndex", scenario.getExampleIndex());
map.put("name", scenario.getName());
map.put("description", scenario.getDescription());
map.put("line", scenario.getLine());
map.put("executorName", executorName);
map.put("startTime", startTime);
map.put("endTime", endTime);
return map;
}

public Map<String, Object> toCucumberJson() {
Map<String, Object> map = new HashMap();
map.put("name", scenario.getName());
Expand Down
Loading
Loading