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

Issue 1793 #1828

Merged
merged 10 commits into from
Nov 14, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class FeatureResult {

private Map<String, Object> resultVariables;
private Map<String, Object> callArg;
private Config config;
private int loopIndex = -1;
private int callDepth;

Expand Down Expand Up @@ -309,6 +310,14 @@ public Map<String, Object> getVariables() {
return resultVariables;
}

public void setConfig(Config config) {
this.config = config;
}

public Config getConfig() {
return config;
}

public void sortScenarioResults() {
Collections.sort(scenarioResults);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public void afterFeature() {
if (lastExecutedScenario != null) {
lastExecutedScenario.engine.invokeAfterHookIfConfigured(true);
result.setVariables(lastExecutedScenario.engine.getAllVariablesAsMap());
result.setConfig(lastExecutedScenario.engine.getConfig());
}
if (!result.isEmpty()) {
for (RuntimeHook hook : suite.hooks) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,16 @@ public Object call(boolean sharedScope, String fileName, Value arg) {
ScenarioEngine engine = getEngine();
Variable called = new Variable(engine.fileReader.readFile(fileName));
Variable result = engine.call(called, arg == null ? null : new Variable(arg), sharedScope);
if (sharedScope && result.isMap()) {
engine.setVariables(result.getValue());
Variable resultVariables = engine.getCallFeatureVariables(result);
if (sharedScope) {
if (resultVariables.isMap()) {
engine.setVariables(resultVariables.getValue());
}
if (result.getValue() instanceof FeatureResult) {
engine.setConfig(((FeatureResult) result.getValue()).getConfig());
}
}
return JsValue.fromJava(result.getValue());
return JsValue.fromJava(resultVariables.getValue());
}

private static Object callSingleResult(ScenarioEngine engine, Object o) throws Exception {
Expand Down Expand Up @@ -232,7 +238,8 @@ public Object callSingle(String fileName, Value arg) throws Exception {
}
Variable resultVar;
try {
resultVar = engine.call(called, argVar, false);
Variable featureResult = engine.call(called, argVar, false);
resultVar = engine.getCallFeatureVariables(featureResult);
} catch (Exception e) {
// don't retain any vestiges of graal-js
RuntimeException re = new RuntimeException(e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ public ScenarioEngine(ScenarioRuntime runtime, Map<String, Variable> vars) {
}

public ScenarioEngine(Config config, ScenarioRuntime runtime, Map<String, Variable> vars, Logger logger) {
this(config, runtime, vars, logger, null);
}

public ScenarioEngine(Config config, ScenarioRuntime runtime, Map<String, Variable> vars, Logger logger, HttpRequestBuilder requestBuilder) {
this.config = config;
this.runtime = runtime;
hooks = runtime.featureRuntime.suite.hooks;
Expand All @@ -126,6 +130,7 @@ public ScenarioEngine(Config config, ScenarioRuntime runtime, Map<String, Variab
bridge = new ScenarioBridge(this);
this.vars = vars;
this.logger = logger;
this.requestBuilder = requestBuilder;
}

public static ScenarioEngine forTempUse(HttpClientFactory hcf) {
Expand Down Expand Up @@ -1297,8 +1302,8 @@ protected <T> Map<String, T> getOrEvalAsMap(Variable var, Object... args) {
public Variable executeFunction(Variable var, Object... args) {
switch (var.type) {
case JS_FUNCTION:
Value jsFunction = var.getValue();
JsValue jsResult = executeJsValue(jsFunction, args);
Value jsFunction = var.getValue();
JsValue jsResult = executeJsValue(JS.attach(jsFunction), args);
return new Variable(jsResult);
case JAVA_FUNCTION: // definitely a "call" with a single argument
Function javaFunction = var.getValue();
Expand Down Expand Up @@ -1985,14 +1990,54 @@ public Variable call(Variable called, Variable arg, boolean sharedScope) {
case FEATURE:
// will be always a map or a list of maps (loop call result)
Object callResult = callFeature(called.getValue(), arg, -1, sharedScope);
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap());
recurseAndAttach("", callResult, seen);
this.rehydrateCallFeatureResult(callResult);
return new Variable(callResult);
default:
throw new RuntimeException("not a callable feature or js function: " + called);
}
}

private void rehydrateCallFeatureResult(Object callResult) {
Object callResultVariables = null;
if (callResult instanceof FeatureResult) {
callResultVariables = ((FeatureResult) callResult).getVariables();
((FeatureResult) callResult).getConfig().detach();
} else if (callResult instanceof List) {
callResultVariables = new ArrayList<Map<String,Object>>();
final List<Map<String,Object>> finalCallResultVariables = (List<Map<String,Object>>)callResultVariables;
((List<?>) callResult).forEach(result -> {
if (result instanceof FeatureResult) {
finalCallResultVariables.add(((FeatureResult) result).getVariables());
Config config = ((FeatureResult) result).getConfig();
config.detach();
}
});
callResultVariables = finalCallResultVariables;
} else {
callResultVariables = callResult;
}
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap());
recurseAndAttach("", callResultVariables, seen);
}

public Variable getCallFeatureVariables(Variable featureResult) {
if (featureResult.getValue() instanceof FeatureResult) {
return new Variable(((FeatureResult) featureResult.getValue()).getVariables());
} else if (featureResult.isList()) {
List resultVariables = new ArrayList();
((List) featureResult.getValue()).forEach(result -> {
if (result instanceof FeatureResult) {
resultVariables.add(this.getCallFeatureVariables(new Variable(result)).getValue());
} else {
resultVariables.add(result);
}
});
return new Variable(resultVariables);
} else {
return featureResult;
}
}

public Variable call(boolean callOnce, String exp, boolean sharedScope) {
StringUtils.Pair pair = parseCallArgs(exp);
Variable called = evalKarateExpression(pair.left);
Expand All @@ -2003,10 +2048,17 @@ public Variable call(boolean callOnce, String exp, boolean sharedScope) {
} else {
result = call(called, arg, sharedScope);
}
if (sharedScope && result.isMap()) {
setVariables(result.getValue());
Variable resultVariables = this.getCallFeatureVariables(result);
if (sharedScope) {
//setVariables(result.getValue());
if (resultVariables.isMap()) {
setVariables(resultVariables.getValue());
}
if (result.getValue() instanceof FeatureResult) {
setConfig(((FeatureResult) result.getValue()).getConfig());
}
}
return result;
return new Variable(resultVariables.getValue());
}

private Variable callOnceResult(ScenarioCall.Result result, boolean sharedScope) {
Expand All @@ -2023,6 +2075,18 @@ private Variable callOnceResult(ScenarioCall.Result result, boolean sharedScope)
logger.warn("[*** callonce result ***] ignoring non-json value: '{}' - {}", k, e.getMessage());
}
});
} else if (result.value != null) {
if (result.value.isMap()) {
((Map) result.value.getValue()).forEach((k, v) -> {
try {
vars.put((String) k, new Variable(v));
} catch (Exception e) {
logger.warn("[*** callonce result ***] ignoring non-json value from result.value: '{}' - {}", k, e.getMessage());
}
});
} else {
logger.warn("[*** callonce result ***] ignoring non-map value from result.value: {}", result.value);
}
}
init(); // this will attach and also insert magic variables
// re-apply config from time of snapshot
Expand Down Expand Up @@ -2062,17 +2126,20 @@ private Variable callOnce(String cacheKey, Variable called, Variable arg, boolea
// this thread is the 'winner'
logger.info(">> lock acquired, begin callonce: {}", cacheKey);
Variable resultValue = call(called, arg, sharedScope);
Variable resultVariables = this.getCallFeatureVariables(resultValue);
// we clone result (and config) here, to snapshot state at the point the callonce was invoked
// detaching is important (see JsFunction) so that we can keep the source-code aside
// and use it to re-create functions in a new JS context - and work around graal-js limitations
Map<String, Variable> clonedVars = called.isFeature() && sharedScope ? detachVariables() : null;
Config clonedConfig = new Config(config);
clonedConfig.detach();
Object resultObject = recurseAndDetachAndShallowClone(resultValue.getValue());
Object resultObject = recurseAndDetachAndShallowClone(resultVariables.getValue());
result = new ScenarioCall.Result(new Variable(resultObject), clonedConfig, clonedVars);
CACHE.put(cacheKey, result);
logger.info("<< lock released, cached callonce: {}", cacheKey);
return resultValue; // another routine will apply globally if needed
// another routine will apply globally if needed
// wrap and attach if being used immediately in a Scenario
return callOnceResult(result, sharedScope);
}
}

Expand All @@ -2092,12 +2159,13 @@ public Object callFeature(Feature feature, Variable arg, int index, boolean shar
// to polute parent scope/context
runtime.engine.recurseAndAttach(runtime.magicVariables);
runtime.engine.recurseAndAttach(runtime.engine.vars);
// todo: shared config
}
if (result.isFailed()) {
KarateException ke = result.getErrorMessagesCombined();
throw ke;
} else {
return result.getVariables();
return result;
}
} else if (arg.isList() || arg.isJsOrJavaFunction()) {
List result = new ArrayList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public boolean tryAdvance(Consumer<? super ScenarioRuntime> action) {
background = new ScenarioRuntime(featureRuntime, currentScenario);
if (background.selectedForExecution) {
background.run();
background.engine.getConfig().detach();
}
if (background.result.isFailed()) { // karate-config.js || background failed
currentScenario = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,23 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, Scenari
perfMode = featureRuntime.perfHook != null;
if (caller.isNone()) {
logAppender = new StringLogAppender(false);
engine = new ScenarioEngine(background == null ? new Config() : background.engine.getConfig(), this, new HashMap(), logger);
Config config = background == null ? new Config() : new Config(background.engine.getConfig());
config.detach();
engine = new ScenarioEngine(config, this, new HashMap(), logger, background != null ? background.engine.requestBuilder.copy() : null);
} else if (caller.isSharedScope()) {
logAppender = caller.parentRuntime.logAppender;
Config config = background == null ? caller.parentRuntime.engine.getConfig() : background.engine.getConfig();
ScenarioEngine parentEngine = background == null ? caller.parentRuntime.engine : background.engine;
Config config = parentEngine.getConfig();
config.detach();
Map<String, Variable> vars = caller.parentRuntime.engine.vars;
engine = new ScenarioEngine(config, this, vars, logger);
engine = new ScenarioEngine(config, this, vars, logger, parentEngine.requestBuilder.copy());
} else { // new, but clone and copy data
logAppender = caller.parentRuntime.logAppender;
Config config = background == null ? new Config(caller.parentRuntime.engine.getConfig()) : background.engine.getConfig();
ScenarioEngine parentEngine = background == null ? caller.parentRuntime.engine : background.engine;
Config config = new Config(parentEngine.getConfig());
config.detach();
// in this case, parent variables are set via magic variables
engine = new ScenarioEngine(config, this, new HashMap(), logger);
engine = new ScenarioEngine(config, this, new HashMap(), logger, parentEngine.requestBuilder.copy());
}
logger.setAppender(logAppender);
actions = new ScenarioActions(engine);
Expand Down Expand Up @@ -355,10 +361,11 @@ public void beforeRun() {
if (this.isDynamicBackground()) {
steps = scenario.getBackgroundSteps();
} else {
steps = scenario.getStepsIncludingBackground();
steps = background == null ? scenario.getStepsIncludingBackground() : scenario.getSteps();
}
ScenarioEngine.set(engine);
engine.init();
engine.getConfig().attach(engine.JS);
if (this.background != null) {
ScenarioEngine backgroundEngine = background.engine;
if (backgroundEngine.driver != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

public class JsCallonceTest {

// @Test
@Test
public void testParallel() {
Results results = Runner.path("classpath:com/intuit/karate/core/jscall/js-callonce.feature")
.configDir("classpath:com/intuit/karate/core/jscall")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ void testParallelOutline() {
.systemProperty("server.port", server.getPort() + "")
.parallel(3);
assertEquals(2, results.getFeaturesPassed());
assertEquals(8, results.getScenariosPassed());
assertEquals(12, results.getScenariosPassed());
assertEquals(0, results.getFailCount());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Feature:
Background:
* url 'http://localhost:' + karate.properties['server.port']

Scenario: reproducing #1835
* call read('parallel-outline-call-api.feature')
* def headers = response.headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function fn() {
return { 'cookie-id': java.lang.System.currentTimeMillis() + '' };
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ function fn() {
config.message2 = result2.message;
var result3 = karate.callSingle('call-single-from-config3.js');
config.sayHello = result3.sayHello;
// attempt at reproducing #1835
karate.call('call-from-config3.feature');
return config;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ Background:
# java object that comes from a callSingle in the config
* def HelloBg = HelloConfigSingle
* callonce read('call-once-from-feature.feature')
# cookies are normalized, so reading a JS function should have no impacts (will read as a null variable)
* configure cookies = read('cookies.js')
* configure afterFeature =
"""
function fn() {
console.log('afterFeature');
}
"""
* configure afterScenario =
"""
function fn() {
console.log('afterScenario');
}
"""

Scenario Outline:
* call read('called.feature')
Expand All @@ -16,10 +30,25 @@ Scenario Outline:
* method get
* status 200
* match response == { message: 'from feature' }


* match HelloBg.sayHello('world') == 'hello world'
* match HelloOnce.sayHello('world') == 'hello world'
* match sayHello('world') == 'hello world'

Examples:
| data |

Scenario Outline: validating background http context set in background will be shared in shared scope, with dynamic scenario outline
* call read('called.feature')
* match functionFromKarateBase() == 'fromKarateBase'
* call read('parallel-outline-call-api.feature')
* match response == { message: 'from feature' }


* match HelloBg.sayHello('world') == 'hello world'
* match HelloOnce.sayHello('world') == 'hello world'
* match sayHello('world') == 'hello world'

Examples:
| data |
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Feature:

Scenario:
* path 'fromfeature'
* method get
* status 200
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,9 @@ void testDynamicOutlineHookNoScenarioExecution() {
assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get("beforeScenario").values().stream().mapToInt(Integer::intValue).sum());
assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get("afterScenario").values().stream().mapToInt(Integer::intValue).sum());

// 5 because steps are added again to each scenario outline to execute again ...
// background steps are re-run on each scenario outline
// so 2 steps per each scenario outline + 1 step that takes to compute the background section
// needed to provide the value on the Examples table
assertEquals(5, testRuntimeHook.getRuntimeHookTracker().get("beforeStep").values().stream().mapToInt(Integer::intValue).sum());
assertEquals(5, testRuntimeHook.getRuntimeHookTracker().get("afterStep").values().stream().mapToInt(Integer::intValue).sum());
assertEquals(3, testRuntimeHook.getRuntimeHookTracker().get("beforeStep").values().stream().mapToInt(Integer::intValue).sum());
assertEquals(3, testRuntimeHook.getRuntimeHookTracker().get("afterStep").values().stream().mapToInt(Integer::intValue).sum());
}

@Test
Expand Down