Skip to content

Commit

Permalink
Add afterScenarioOutline hook
Browse files Browse the repository at this point in the history
  • Loading branch information
OwenK2 committed Dec 24, 2024
1 parent c79e31b commit 0da7b56
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 13 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2252,6 +2252,7 @@ You can adjust configuration settings for the HTTP client used by Karate using t
`printEnabled` | boolean | Can be used to suppress the [`print`](#print) output when not in 'dev mode' by setting as `false` (default `true`)
`report` | JSON / boolean | see [report verbosity](#report-verbosity)
`afterScenario` | JS function | Will be called [after every `Scenario`](#hooks) (or `Example` within a `Scenario Outline`), refer to this example: [`hooks.feature`](karate-demo/src/test/java/demo/hooks/hooks.feature)
`afterScenarioOutline` | JS function | Will be called [after every `Scenario Outline`](#hooks). Is called after the last `afterScenario` for the last scenario in the outline. Refer to this example: [`hooks.feature`](karate-demo/src/test/java/demo/hooks/hooks.feature)
`afterFeature` | JS function | Will be called [after every `Feature`](#hooks), refer to this example: [`hooks.feature`](karate-demo/src/test/java/demo/hooks/hooks.feature)
`ssl` | boolean | Enable HTTPS calls without needing to configure a trusted certificate or key-store.
`ssl` | string | Like above, but force the SSL algorithm to one of [these values](http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext). (The above form internally defaults to `TLS` if simply set to `true`).
Expand Down Expand Up @@ -4441,6 +4442,7 @@ Before *everything* (or 'globally' once) | See [`karate.callSingle()`](#karateca
Before every `Scenario` | Use the [`Background`](#script-structure). Note that [`karate-config.js`](#karate-configjs) is processed before *every* `Scenario` - so you can choose to put "global" config here, for example using [`karate.configure()`](#karate-configure).
Once (or at the start of) every `Feature` | Use a [`callonce`](#callonce) in the [`Background`](#script-structure). The advantage is that you can set up variables (using [`def`](#def) if needed) which can be used in all `Scenario`-s within that `Feature`.
After every `Scenario` | [`configure afterScenario`](#configure) (see [example](karate-demo/src/test/java/demo/hooks/hooks.feature))
After every `Scenario Outline` | [`configure afterScenarioOutline`](#configure) (see [example](karate-demo/src/test/java/demo/hooks/hooks.feature))
At the end of the `Feature` | [`configure afterFeature`](#configure) (see [example](karate-demo/src/test/java/demo/hooks/hooks.feature))

> Note that for the `afterFeature` hook to work, you should be using the [`Runner` API](#parallel-execution) and not the JUnit runner.
Expand Down
4 changes: 4 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/RuntimeHook.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ default void afterScenario(ScenarioRuntime sr) {

}

default void afterScenarioOutline(ScenarioRuntime sr) {

}

default boolean beforeFeature(FeatureRuntime fr) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* The MIT License
*
* Copyright 2022 Karate Labs Inc.
*
* 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;

/**
*
* @author OwenK2
*/
public enum AfterHookType {

AFTER_SCENARIO("afterScenario"),
AFTER_OUTLINE("afterScenarioOutline"),
AFTER_FEATURE("afterFeature");

private String prefix;

private AfterHookType(String prefix) {
this.prefix = prefix;
}

public String getPrefix() {
return prefix;
}
}
13 changes: 13 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/core/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public class Config {
private HttpLogModifier logModifier;

private Variable afterScenario = Variable.NULL;
private Variable afterScenarioOutline = Variable.NULL;
private Variable afterFeature = Variable.NULL;
private Variable headers = Variable.NULL;
private Variable cookies = Variable.NULL;
Expand Down Expand Up @@ -175,6 +176,9 @@ public boolean configure(String key, Variable value) { // TODO use enum
case "afterScenario":
afterScenario = value;
return false;
case "afterScenarioOutline":
afterScenarioOutline = value;
return false;
case "afterFeature":
afterFeature = value;
return false;
Expand Down Expand Up @@ -382,6 +386,7 @@ public Config(Config parent) {
cookies = parent.cookies;
responseHeaders = parent.responseHeaders;
afterScenario = parent.afterScenario;
afterScenarioOutline = parent.afterScenarioOutline;
afterFeature = parent.afterFeature;
continueOnStepFailureMethods = parent.continueOnStepFailureMethods;
continueAfterContinueOnStepFailure = parent.continueAfterContinueOnStepFailure;
Expand Down Expand Up @@ -538,6 +543,14 @@ public void setAfterScenario(Variable afterScenario) {
this.afterScenario = afterScenario;
}

public Variable getAfterScenarioOutline() {
return afterScenarioOutline;
}

public void setAfterScenarioOutline(Variable afterScenarioOutline) {
this.afterScenarioOutline = afterScenarioOutline;
}

public Variable getAfterFeature() {
return afterFeature;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,38 @@ private void processScenario(ScenarioRuntime sr) {
if (!sr.result.getStepResults().isEmpty()) {
synchronized (result) {
result.addResult(sr.result);

// Execute afterScenarioOutline if applicable
// NOTE: Needs to be run after adding result, since result count is used to deterime
// if the scenario is the last in the outline
if (!sr.dryRun && isLastScenarioInOutline(sr.scenario)) {
sr.engine.invokeAfterHookIfConfigured(AfterHookType.AFTER_OUTLINE);
suite.hooks.forEach(h -> h.afterScenarioOutline(sr));
}
}
}
}
}

private boolean isLastScenarioInOutline(Scenario scenario) {
// Check if scenario is part of an outline
if (!scenario.isOutlineExample()) return false;

// Count the number of completed scenarios with the same section ID (in same outline)
int completedScenarios = 0;
for (ScenarioResult result : result.getScenarioResults()) {
if (result.getScenario().getSection().getIndex() == scenario.getSection().getIndex()) {
completedScenarios++;
}
}
return completedScenarios == scenario.getSection().getScenarioOutline().getNumScenarios();
}

// extracted for junit5
public synchronized void afterFeature() {
result.sortScenarioResults();
if (lastExecutedScenario != null) {
lastExecutedScenario.engine.invokeAfterHookIfConfigured(true);
lastExecutedScenario.engine.invokeAfterHookIfConfigured(AfterHookType.AFTER_FEATURE);
result.setVariables(lastExecutedScenario.engine.getAllVariablesAsMap());
result.setConfig(lastExecutedScenario.engine.getConfig());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,20 +235,35 @@ public void print(String exp) {
evalJs("karate.log('[print]'," + exp + ")");
}

public void invokeAfterHookIfConfigured(boolean afterFeature) {
public void invokeAfterHookIfConfigured(AfterHookType hookType) {
// Do not call hooks on "called" scenarios/features
if (runtime.caller.depth > 0) {
return;
}
Variable v = afterFeature ? config.getAfterFeature() : config.getAfterScenario();

// Get hook variable based on type
Variable v;
switch (hookType) {
case AFTER_SCENARIO:
v = config.getAfterScenario();
break;
case AFTER_OUTLINE:
v = config.getAfterScenarioOutline();
break;
case AFTER_FEATURE:
v = config.getAfterFeature();
break;
default: return;
}

if (v.isJsOrJavaFunction()) {
if (afterFeature) {
if (hookType == AfterHookType.AFTER_FEATURE) {
ScenarioEngine.set(this); // for any bridge / js to work
}
try {
executeFunction(v);
} catch (Exception e) {
String prefix = afterFeature ? "afterFeature" : "afterScenario";
logger.warn("{} hook failed: {}", prefix, e + "");
logger.warn("{} hook failed: {}", hookType.getPrefix(), e + "");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@ public Map<String, Object> toKarateJson() {
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 -> {
boolean needToAddRecent = runtime.result != null;
for(ScenarioResult result : runtime.featureRuntime.result.getScenarioResults()) {
if (result.getScenario().getSection().getIndex() == scenarioOutline.getSection().getIndex()) {
scenarioResults.add(result.toInfoJson());
if(result.equals(runtime.result)) {
needToAddRecent = false;
}
}
});
}

// Add most recent result
if (runtime.result != null) {
// Add most recent result if we haven't already (and it's not null)
if (needToAddRecent) {
scenarioResults.add(runtime.result.toInfoJson());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ public int compareTo(ScenarioResult sr) {
return scenario.getExampleIndex() - sr.scenario.getExampleIndex();
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return compareTo((ScenarioResult)obj) == 0;
}

public String getFailureMessageForDisplay() {
if (failedStep == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ public void afterRun() {
currentStepResult = result.addFakeStepResult("no steps executed", null);
}
if (!dryRun) {
engine.invokeAfterHookIfConfigured(false);
engine.invokeAfterHookIfConfigured(AfterHookType.AFTER_SCENARIO);
featureRuntime.suite.hooks.forEach(h -> h.afterScenario(this));
engine.stop(currentStepResult);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Background:
function fn() {
console.log('afterFeature');
}
"""
* configure afterScenarioOutline =
"""
function fn() {
console.log('afterScenarioOutline');
}
"""
* configure afterScenario =
"""
Expand Down
2 changes: 1 addition & 1 deletion karate-demo/src/test/java/demo/hooks/called.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Feature:

Background:
# 'afterScenario' and 'afterFeature' are NOT supported when a feature is called
# 'afterScenario', 'afterScenarioOutline', and 'afterFeature' are NOT supported when a feature is called
# so this will have no effect, UNLESS this feature is run directly
* configure afterScenario = function(){ karate.log('end called scenario') }

Expand Down
13 changes: 12 additions & 1 deletion karate-demo/src/test/java/demo/hooks/hooks.feature
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Feature: demo karate's equivalent of before and after hooks
note that 'afterScenario' / 'afterFeature' if set up using 'configure'
note that 'afterScenario' / 'afterScenarioOutline' / 'afterFeature' if set up using 'configure'
is not supported within features invoked using the 'call' or 'callonce' keywords

Background:
Expand Down Expand Up @@ -28,6 +28,17 @@ function(){

* configure afterFeature = function(){ karate.call('after-feature.feature'); }

# Only runs at the end of a scenario outline after all examples have been run
# This hook will be called after the last scenario in the outline is executed
# It will also run after any configured 'afterScenario's for that outline
# NOTE if using parallel, last Scenario executed may not be the last example in the outline
* configure afterScenarioOutline =
"""
function(){
karate.log('after scenario outline:', karate.scenarioOutline.name);
}
"""

Scenario: first
* print foo

Expand Down

0 comments on commit 0da7b56

Please sign in to comment.