From d99e5a4f13e6ea947427f8b68615121c75b37436 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Sat, 10 Oct 2020 12:21:07 +0530 Subject: [PATCH] [rewrite] #1281 wip call feature loop also new support for function todo graal function context switching --- .../intuit/karate/runtime/ScenarioCall.java | 17 +++- .../intuit/karate/runtime/ScenarioEngine.java | 80 +++++++++++++++---- .../karate/runtime/ScenarioRuntime.java | 26 +++--- .../com/intuit/karate/runtime/Variable.java | 6 +- .../karate/runtime/ScenarioRuntimeTest.java | 18 ++++- 5 files changed, 116 insertions(+), 31 deletions(-) diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioCall.java b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioCall.java index 1932b0332..444801a60 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioCall.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioCall.java @@ -42,6 +42,19 @@ public class ScenarioCall { private Variable arg; private boolean globalScope; private boolean karateConfigDisabled; + private int loopIndex = -1; + + public boolean isNone() { + return depth == 0; + } + + public int getLoopIndex() { + return loopIndex; + } + + public void setLoopIndex(int loopIndex) { + this.loopIndex = loopIndex; + } public void setGlobalScope(boolean globalScope) { this.globalScope = globalScope; @@ -63,10 +76,6 @@ public boolean isCallonce() { return callonce; } - public boolean isNone() { - return depth == 0; - } - public void setCallonce(boolean callonce) { this.callonce = callonce; } diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java index 26c5f039b..002eed9de 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java @@ -30,6 +30,7 @@ import com.intuit.karate.core.Feature; import com.intuit.karate.data.Json; import com.intuit.karate.data.JsonUtils; +import com.intuit.karate.exception.KarateException; import com.intuit.karate.graal.JsEngine; import com.intuit.karate.match.Match; import com.intuit.karate.match.MatchResult; @@ -77,7 +78,7 @@ public Variable eval(String exp) { return new Variable(JS.eval(exp)); } - public void putHidden(String key, Object value) { + public void setHiddenVariable(String key, Object value) { if (value instanceof Variable) { JS.put(key, ((Variable) value).getValue()); } else { @@ -85,7 +86,7 @@ public void putHidden(String key, Object value) { } } - public void put(String key, Object value) { + public void setVariable(String key, Object value) { if (value instanceof Variable) { vars.put(key, (Variable) value); } else { @@ -93,11 +94,11 @@ public void put(String key, Object value) { } } - public void putAll(Map map) { + public void setVariables(Map map) { if (map == null) { return; } - map.forEach((k, v) -> put(k, v)); + map.forEach((k, v) -> setVariable(k, v)); } public Map copyVariables(boolean deep) { @@ -158,9 +159,9 @@ public void assign(AssignType assignType, String name, String exp, boolean valid } } if (assignType == AssignType.TEXT) { - put(name, exp); + setVariable(name, exp); } else { - put(name, evalAndCastTo(assignType, exp)); + setVariable(name, evalAndCastTo(assignType, exp)); } } @@ -782,19 +783,70 @@ public Variable call(boolean callOnce, String exp, boolean reuseParentConfig) { case JS_FUNCTION: return arg == null ? called.invokeFunction() : called.invokeFunction(new Object[]{arg.getValue()}); case KARATE_FEATURE: - return callFeature(called.getValue(), arg, reuseParentConfig); + ScenarioRuntime runtime = ScenarioRuntime.LOCAL.get(); + return callFeature(runtime, called.getValue(), arg, -1, reuseParentConfig); default: throw new RuntimeException("not a callable feature or js function: " + called); } } - public Variable callFeature(Feature feature, Variable arg, boolean reuseParentConfig) { - ScenarioRuntime runtime = ScenarioRuntime.LOCAL.get(); - ScenarioCall call = new ScenarioCall(runtime, feature); - call.setArg(arg); - FeatureRuntime fr = new FeatureRuntime(call); - fr.run(); - return fr.getResultVariable(); + public Variable callFeature(ScenarioRuntime runtime, Feature feature, Variable arg, int index, boolean reuseParentConfig) { + if (arg == null || arg.isMap()) { + ScenarioCall call = new ScenarioCall(runtime, feature); + call.setArg(arg); + call.setLoopIndex(index); + FeatureRuntime fr = new FeatureRuntime(call); + fr.run(); + if (fr.result.isFailed()) { + KarateException ke = fr.result.getErrorsCombined(); + if (index == -1) { + runtime.logError(ke.getMessage()); + } + throw ke; + } else { + return fr.getResultVariable(); + } + } else if (arg.isList() || arg.isFunction()) { + List result = new ArrayList(); + List errors = new ArrayList(); + int loopIndex = 0; + boolean isList = arg.isList(); + Iterator iterator = isList ? arg.getValue().iterator() : null; + while (true) { + Variable loopArg; + if (isList) { + loopArg = iterator.hasNext() ? new Variable(iterator.next()) : Variable.NULL; + } else { // function + loopArg = arg.invokeFunction(loopIndex); + } + if (!loopArg.isMap()) { + if (!isList) { + logger.info("feature call loop function ended at index {}, returned: {}", loopIndex, loopArg); + } + break; + } + try { + Variable loopResult = callFeature(runtime, feature, loopArg, loopIndex, reuseParentConfig); + result.add(loopResult.getValue()); + } catch (Exception e) { + String message = "feature call loop failed at index: " + loopIndex + ", " + e.getMessage(); + errors.add(message); + runtime.logError(message); + if (!isList) { // this is a generator function, abort infinite loop ! + break; + } + } + loopIndex++; + } + if (errors.isEmpty()) { + return new Variable(result); + } else { + String errorMessage = StringUtils.join(errors, '\n'); + throw new KarateException(errorMessage); + } + } else { + throw new RuntimeException("feature call argument is not a json object or array: " + arg); + } } public Variable evalJsonPath(Variable v, String path) { diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java index 763cb8cda..475968510 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java @@ -147,7 +147,7 @@ private int nextStepIndex() { return stepIndex++; } - private void logError(String message) { + protected void logError(String message) { if (currentStep != null) { message = currentStep.getDebugInfo() + "\n" + currentStep.toString() @@ -197,18 +197,26 @@ public void beforeRun() { logger.setAppender(appender); LOCAL.set(this); engine.init(); - engine.putHidden(VariableNames.KARATE, bridge); - engine.putHidden(VariableNames.READ, readFunction); + engine.setHiddenVariable(VariableNames.KARATE, bridge); + engine.setHiddenVariable(VariableNames.READ, readFunction); if (scenario.isDynamic()) { steps = scenario.getBackgroundSteps(); } else { steps = background == null ? scenario.getStepsIncludingBackground() : scenario.getSteps(); if (scenario.isOutline()) { // init examples row magic variables Map exampleData = scenario.getExampleData(); - engine.put("__row", exampleData); - engine.put("__num", scenario.getExampleIndex()); - // TODO breaking change configure outlineVariablesAuto - exampleData.forEach((k, v) -> engine.put(k, v)); + exampleData.forEach((k, v) -> engine.setVariable(k, v)); + engine.setVariable("__row", exampleData); + engine.setVariable("__num", scenario.getExampleIndex()); + // TODO breaking change configure outlineVariablesAuto + } + if (!parentCall.isNone()) { + Variable arg = parentCall.getArg(); + engine.setVariable("__arg", arg); + engine.setVariable("__loop", parentCall.getLoopIndex()); + if (arg != null && arg.isMap()) { + engine.setVariables(arg.getValue()); + } } } result.setThreadName(Thread.currentThread().getName()); @@ -225,7 +233,7 @@ private void evalConfigJs(String js) { if (js != null) { Variable fun = engine.evalKarateExpression(js); if (fun.isFunction()) { - engine.putAll(fun.evalAsMap()); + engine.setVariables(fun.evalAsMap()); } else { logger.warn("config did not evaluate to js function: {}", js); } @@ -291,7 +299,7 @@ public void afterRun() { public void call(boolean callOnce, String line) { Variable v = engine.call(callOnce, line, true); if (v.isMap()) { - engine.putAll(v.getValue()); + engine.setVariables(v.getValue()); } } diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java b/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java index 46d102666..ca5d1063a 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java @@ -101,7 +101,7 @@ public Variable(Object o) { public T getValue() { return (T) value; } - + public boolean isBytes() { return type == Type.BYTES; } @@ -157,7 +157,7 @@ public Variable invokeFunction(Object... args) { return new Variable(result); } } - + public Map evalAsMap() { if (isFunction()) { Variable v = invokeFunction(); @@ -165,7 +165,7 @@ public Map evalAsMap() { } else { return isMap() ? getValue() : null; } - } + } public Node getAsXml() { switch (type) { diff --git a/karate-core2/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java b/karate-core2/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java index 85b340344..e0023ff64 100644 --- a/karate-core2/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java +++ b/karate-core2/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java @@ -87,7 +87,23 @@ void testCallKarateFeature() { "def b = 'bar'", "def res = call read('called1.feature')" ); - matchVarEquals("res", "{ a: 1, b: 'bar', foo: { hello: 'world' }, configSource: 'normal' }"); + matchVarEquals("res", "{ a: 1, b: 'bar', foo: { hello: 'world' }, configSource: 'normal', __arg: null, __loop: -1 }"); + run( + "def b = 'bar'", + "def res = call read('called1.feature') { foo: 'bar' }" + ); + matchVarEquals("res", "{ a: 1, b: 'bar', foo: { hello: 'world' }, configSource: 'normal', __arg: { foo: 'bar' }, __loop: -1 }"); + run( + "def b = 'bar'", + "def res = call read('called1.feature') [{ foo: 'bar' }]" + ); + matchVarEquals("res", "[{ a: 1, b: 'bar', foo: { hello: 'world' }, configSource: 'normal', __arg: { foo: 'bar' }, __loop: 0 }]"); +// run( +// "def b = 'bar'", +// "def fun = function(i){ return { index: i } }", +// "def res = call read('called1.feature') fun" +// ); +// matchVarEquals("res", "[{ a: 1, b: 'bar', foo: { hello: 'world' }, configSource: 'normal', __arg: { index: 0 }, __loop: 0 }]"); } }