diff --git a/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java b/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java index 64f79975b..cb54e648b 100644 --- a/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java +++ b/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java @@ -25,6 +25,7 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.ScriptValue; +import com.intuit.karate.graal.JsValue; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.spi.json.JsonProvider; @@ -35,6 +36,7 @@ import de.siegmar.fastcsv.reader.CsvReader; import de.siegmar.fastcsv.reader.CsvRow; import de.siegmar.fastcsv.writer.CsvWriter; +import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; @@ -42,8 +44,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +import net.minidev.json.JSONStyle; import net.minidev.json.JSONValue; import net.minidev.json.parser.JSONParser; +import net.minidev.json.reader.JsonWriter; +import net.minidev.json.reader.JsonWriterI; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -57,10 +62,19 @@ private JsonUtils() { // only static methods } + private static class JsValueWriter implements JsonWriterI { + + @Override + public void writeJSONString(E value, Appendable out, JSONStyle compression) throws IOException { + JsonWriter.toStringWriter.writeJSONString("\"#" + value.type + "\"", out, compression); + } + + } + static { + JSONValue.registerWriter(JsValue.class, new JsValueWriter()); // ensure that even if jackson (databind?) is on the classpath, don't switch provider Configuration.setDefaults(new Configuration.Defaults() { - private final JsonProvider jsonProvider = new JsonSmartJsonProvider(); private final MappingProvider mappingProvider = new JsonSmartMappingProvider(); diff --git a/karate-core2/src/main/java/com/intuit/karate/graal/JsEngine.java b/karate-core2/src/main/java/com/intuit/karate/graal/JsEngine.java index 0acd2a270..bfa7534c0 100644 --- a/karate-core2/src/main/java/com/intuit/karate/graal/JsEngine.java +++ b/karate-core2/src/main/java/com/intuit/karate/graal/JsEngine.java @@ -63,12 +63,7 @@ private static class JsContext { } Value eval(String exp) { - try { - return context.eval(JS, exp); - } catch (Exception e) { - logger.error("js eval failed: {} - {}", e, exp); - throw new RuntimeException(e); - } + return context.eval(JS, exp); } } @@ -91,11 +86,14 @@ public static void remove() { public static JsEngine local() { Engine engine = GLOBAL_JS_CONTEXT.get().context.getEngine(); - JsEngine je = new JsEngine(new JsContext(engine)); - Value bindings = global().bindings(); + return new JsEngine(new JsContext(engine)); + } + + public static JsEngine localWithGlobalBindings() { + JsEngine je = local(); + Value bindings = global().jc.bindings; for (String key : bindings.getMemberKeys()) { - Value v = bindings.getMember(key); - je.putValue(key, v); + je.putValue(key, bindings.getMember(key)); } return je; } @@ -114,6 +112,10 @@ private JsEngine(JsContext sc) { this.jc = sc; } + public Context getGraalContext() { + return jc.context; + } + public Value bindings() { return jc.bindings; } @@ -141,7 +143,7 @@ public void put(String key, Object value) { public void putAll(Map map) { map.forEach((k, v) -> put(k, v)); } - + public boolean hasMember(String key) { return jc.bindings.hasMember(key); } @@ -165,10 +167,10 @@ public String toJson(JsValue jv) { public void putValue(String key, Value v) { if (v.isHostObject()) { - bindings().putMember(key, v.asHostObject()); + jc.bindings.putMember(key, v.asHostObject()); } else if (v.canExecute()) { Value fun = evalForValue("(" + v.toString() + ")"); - bindings().putMember(key, fun); + jc.bindings.putMember(key, fun); } else { put(key, JsValue.toJava(v)); } diff --git a/karate-core2/src/main/java/com/intuit/karate/graal/JsValue.java b/karate-core2/src/main/java/com/intuit/karate/graal/JsValue.java index 53b5f81e5..5f6bbf6eb 100644 --- a/karate-core2/src/main/java/com/intuit/karate/graal/JsValue.java +++ b/karate-core2/src/main/java/com/intuit/karate/graal/JsValue.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; /** @@ -45,7 +46,7 @@ public static enum Type { OTHER } - private final Value original; + private Value original; private final Object value; public final Type type; @@ -109,10 +110,22 @@ public List getAsList() { return (List) value; } + public void switchContext(JsEngine js) { + Context context = original.getContext(); + if (context != null && !context.equals(js.getGraalContext())) { + String temp = "(" + original.toString() + ")"; + original = js.evalForValue(temp); + } + } + public Value getOriginal() { return original; } + public void setOriginal(Value original) { + this.original = original; + } + public JsValue execute(Object... args) { return new JsValue(original.execute(args)); } @@ -140,10 +153,6 @@ public boolean isOther() { return type == Type.OTHER; } - public String asString() { - return original.asString(); - } - @Override public String toString() { return original.toString(); diff --git a/karate-core2/src/main/java/com/intuit/karate/match/Match.java b/karate-core2/src/main/java/com/intuit/karate/match/Match.java index 71e5d5347..5dc53972d 100644 --- a/karate-core2/src/main/java/com/intuit/karate/match/Match.java +++ b/karate-core2/src/main/java/com/intuit/karate/match/Match.java @@ -23,6 +23,7 @@ */ package com.intuit.karate.match; +import com.intuit.karate.graal.JsEngine; import static com.intuit.karate.match.MatchResult.*; import java.util.HashMap; import java.util.Map; @@ -69,8 +70,8 @@ public static MatchValue that(Object o) { return new MatchValue(MatchValue.parseIfJsonOrXml(o)); } - public static MatchResult execute(MatchType matchType, MatchValue actual, MatchValue expected) { - MatchOperation mo = new MatchOperation(matchType, actual, expected); + public static MatchResult execute(JsEngine js, MatchType matchType, MatchValue actual, MatchValue expected) { + MatchOperation mo = new MatchOperation(js, matchType, actual, expected); mo.execute(); if (mo.pass) { return PASS; diff --git a/karate-core2/src/main/java/com/intuit/karate/match/MatchContext.java b/karate-core2/src/main/java/com/intuit/karate/match/MatchContext.java index abac2c790..f2577443f 100644 --- a/karate-core2/src/main/java/com/intuit/karate/match/MatchContext.java +++ b/karate-core2/src/main/java/com/intuit/karate/match/MatchContext.java @@ -23,12 +23,15 @@ */ package com.intuit.karate.match; +import com.intuit.karate.graal.JsEngine; + /** * * @author pthomas3 */ public class MatchContext { + public final JsEngine JS; public final MatchOperation root; public final int depth; public final boolean xml; @@ -36,7 +39,8 @@ public class MatchContext { public final String name; public final int index; - protected MatchContext(MatchOperation root, boolean xml, int depth, String path, String name, int index) { + protected MatchContext(JsEngine js, MatchOperation root, boolean xml, int depth, String path, String name, int index) { + this.JS = js; this.root = root; this.xml = xml; this.depth = depth; @@ -48,19 +52,19 @@ protected MatchContext(MatchOperation root, boolean xml, int depth, String path, public MatchContext descend(String name) { if (xml) { String childPath = path.endsWith("/@") ? path + name : (depth == 0 ? "" : path) + "/" + name; - return new MatchContext(root, xml, depth + 1, childPath, name, -1); + return new MatchContext(JS, root, xml, depth + 1, childPath, name, -1); } else { boolean needsQuotes = name.indexOf('-') != -1 || name.indexOf(' ') != -1 || name.indexOf('.') != -1; String childPath = needsQuotes ? path + "['" + name + "']" : path + '.' + name; - return new MatchContext(root, xml, depth + 1, childPath, name, -1); + return new MatchContext(JS, root, xml, depth + 1, childPath, name, -1); } } public MatchContext descend(int index) { if (xml) { - return new MatchContext(root, xml, depth + 1, path + "[" + (index + 1) + "]", name, index); + return new MatchContext(JS, root, xml, depth + 1, path + "[" + (index + 1) + "]", name, index); } else { - return new MatchContext(root, xml, depth + 1, path + "[" + index + "]", name, index); + return new MatchContext(JS, root, xml, depth + 1, path + "[" + index + "]", name, index); } } diff --git a/karate-core2/src/main/java/com/intuit/karate/match/MatchOperation.java b/karate-core2/src/main/java/com/intuit/karate/match/MatchOperation.java index f4bc35bdc..8928db135 100644 --- a/karate-core2/src/main/java/com/intuit/karate/match/MatchOperation.java +++ b/karate-core2/src/main/java/com/intuit/karate/match/MatchOperation.java @@ -53,23 +53,33 @@ public class MatchOperation { boolean pass = true; String failReason; - + public MatchOperation(MatchType type, MatchValue actual, MatchValue expected) { - this(null, type, actual, expected); + this(JsEngine.global(), null, type, actual, expected); + } + + public MatchOperation(JsEngine js, MatchType type, MatchValue actual, MatchValue expected) { + this(js, null, type, actual, expected); + } + + private MatchOperation(MatchContext context, MatchType type, MatchValue actual, MatchValue expected) { + this(null, context, type, actual, expected); } - public MatchOperation(MatchContext context, MatchType type, MatchValue actual, MatchValue expected) { + private MatchOperation(JsEngine js, MatchContext context, MatchType type, MatchValue actual, MatchValue expected) { this.type = type; this.actual = actual; this.expected = expected; if (context == null) { + if (js == null) { + js = JsEngine.global(); + } this.failures = new ArrayList(); if (actual.isXml()) { - this.context = new MatchContext(this, true, 0, "/", "", -1); + this.context = new MatchContext(js, this, true, 0, "/", "", -1); } else { - this.context = new MatchContext(this, false, 0, "$", "", -1); + this.context = new MatchContext(js, this, false, 0, "$", "", -1); } - } else { this.context = context; this.failures = context.root.failures; @@ -149,10 +159,9 @@ public boolean execute() { List list = actual.getValue(); MatchType nestedMatchType = fromMatchEach(); int count = list.size(); - JsEngine jsEngine = JsEngine.global(); for (int i = 0; i < count; i++) { Object o = list.get(i); - jsEngine.put("_$", o); + context.JS.put("_$", o); MatchOperation mo = new MatchOperation(context.descend(i), nestedMatchType, new MatchValue(o), expected); mo.execute(); if (!mo.pass) { @@ -220,7 +229,7 @@ private boolean macroEqualsExpected(String expStr) { MatchType nestedType = macroToMatchType(false, macro); int startPos = matchTypeToStartPos(nestedType); macro = macro.substring(startPos); - JsValue jv = JsEngine.evalGlobal(macro); + JsValue jv = context.JS.eval(macro); MatchOperation mo = new MatchOperation(context, nestedType, actual, new MatchValue(jv.getValue())); return mo.execute(); } else if (macro.startsWith("[")) { @@ -233,16 +242,15 @@ private boolean macroEqualsExpected(String expStr) { String bracketContents = macro.substring(1, closeBracketPos); List listAct = actual.getValue(); int listSize = listAct.size(); - JsEngine jsEngine = JsEngine.global(); - jsEngine.put("$", context.root.actual.getValue()); - jsEngine.put("_", listSize); + context.JS.put("$", context.root.actual.getValue()); + context.JS.put("_", listSize); String sizeExpr; if (bracketContents.indexOf('_') != -1) { // #[_ < 5] sizeExpr = bracketContents; } else { // #[5] | #[$.foo] sizeExpr = bracketContents + " == _"; } - JsValue jv = jsEngine.eval(sizeExpr); + JsValue jv = context.JS.eval(sizeExpr); if (!jv.isTrue()) { return fail("actual array length is " + listSize); } @@ -264,7 +272,7 @@ private boolean macroEqualsExpected(String expStr) { MatchType nestedType = macroToMatchType(true, macro); // match each int startPos = matchTypeToStartPos(nestedType); macro = macro.substring(startPos); - JsValue jv = JsEngine.evalGlobal(macro); + JsValue jv = context.JS.eval(macro); MatchOperation mo = new MatchOperation(context, nestedType, actual, new MatchValue(jv.getValue())); return mo.execute(); } @@ -313,10 +321,9 @@ private boolean macroEqualsExpected(String expStr) { } macro = StringUtils.trimToNull(macro); if (macro != null && questionPos != -1) { - JsEngine jsEngine = JsEngine.global(); - jsEngine.put("$", context.root.actual.getValue()); - jsEngine.put("_", actual.getValue()); - JsValue jv = jsEngine.eval(macro); + context.JS.put("$", context.root.actual.getValue()); + context.JS.put("_", actual.getValue()); + JsValue jv = context.JS.eval(macro); if (!jv.isTrue()) { return fail("evaluated to 'false'"); } @@ -570,8 +577,9 @@ private static String collectFailureReasons(MatchOperation root) { sb.append(prefix).append(mo.actual.getAsXmlString()).append('\n'); sb.append(prefix).append(mo.expected.getAsXmlString()).append('\n'); } else { + MatchValue expected = mo.expected.getSortedLike(mo.actual); sb.append(prefix).append(mo.actual.getWithinSingleQuotesIfString()).append('\n'); - sb.append(prefix).append(mo.expected.getWithinSingleQuotesIfString()).append('\n'); + sb.append(prefix).append(expected.getWithinSingleQuotesIfString()).append('\n'); } } return sb.toString(); diff --git a/karate-core2/src/main/java/com/intuit/karate/match/MatchValue.java b/karate-core2/src/main/java/com/intuit/karate/match/MatchValue.java index 6fbe688a6..24d5869f7 100644 --- a/karate-core2/src/main/java/com/intuit/karate/match/MatchValue.java +++ b/karate-core2/src/main/java/com/intuit/karate/match/MatchValue.java @@ -26,8 +26,11 @@ import com.intuit.karate.XmlUtils; import com.intuit.karate.data.Json; import com.intuit.karate.data.JsonUtils; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.w3c.dom.Node; /** @@ -177,6 +180,27 @@ public String getAsXmlString() { return getAsString(); } } + + public MatchValue getSortedLike(MatchValue other) { + if (isMap() && other.isMap()) { + Map reference = other.getValue(); + Map source = getValue(); + Set remainder = new LinkedHashSet(source.keySet()); + Map result = new LinkedHashMap(source.size()); + reference.keySet().forEach(key -> { + if (source.containsKey(key)) { + result.put(key, source.get(key)); + remainder.remove(key); + } + }); + for (String key : remainder) { + result.put(key, source.get(key)); + } + return new MatchValue(result); + } else { + return this; + } + } @Override public String toString() { diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/FeatureRuntime.java b/karate-core2/src/main/java/com/intuit/karate/runtime/FeatureRuntime.java index e52024b0a..64d64ea1c 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/FeatureRuntime.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/FeatureRuntime.java @@ -26,30 +26,34 @@ import com.intuit.karate.core.Feature; import com.intuit.karate.core.FeatureResult; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; /** * * @author pthomas3 */ public class FeatureRuntime implements Runnable { - + public final SuiteRuntime suite; public final FeatureRuntime rootFeature; public final ScenarioCall parentCall; public final Feature feature; public final FeatureResult result; private final ScenarioGenerator scenarios; - + + public final Map FEATURE_CACHE = new HashMap(); + private PerfRuntime perfRuntime; - + public Path getParentPath() { return feature.getPath().getParent(); } - + public Path getRootParentPath() { return rootFeature.getParentPath(); } - + public Path getPath() { return feature.getPath(); } @@ -57,20 +61,20 @@ public Path getPath() { public void setPerfRuntime(PerfRuntime perfRuntime) { this.perfRuntime = perfRuntime; } - + public boolean isPerfMode() { return perfRuntime != null; } public PerfRuntime getPerfRuntime() { return perfRuntime; - } - + } + public FeatureRuntime(SuiteRuntime suite, Feature feature, boolean karateConfigDisabled) { this(suite, feature, ScenarioCall.NONE); parentCall.setKarateConfigDisabled(karateConfigDisabled); } - + public FeatureRuntime(ScenarioCall call) { this(call.parentRuntime.featureRuntime.suite, call.feature, call); } @@ -83,9 +87,9 @@ private FeatureRuntime(SuiteRuntime suite, Feature feature, ScenarioCall parentC result = new FeatureResult(suite.results, feature); scenarios = new ScenarioGenerator(this, feature.getSections().iterator()); } - + private ScenarioRuntime currentScenario; - + public Variable getResultVariable() { if (currentScenario == null) { return Variable.NULL; 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 444801a60..28e9f4135 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 @@ -100,12 +100,12 @@ public ScenarioCall(ScenarioRuntime parentRuntime, Feature feature) { public static class Result { - public final Variable result; + public final Variable value; public final Config config; public final Map vars; - public Result(Variable result, Config config, Map vars) { - this.result = result; + public Result(Variable value, Config config, Map vars) { + this.value = value; this.config = config; this.vars = vars; } 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 002eed9de..0d97758c3 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 @@ -32,6 +32,7 @@ import com.intuit.karate.data.JsonUtils; import com.intuit.karate.exception.KarateException; import com.intuit.karate.graal.JsEngine; +import com.intuit.karate.graal.JsValue; import com.intuit.karate.match.Match; import com.intuit.karate.match.MatchResult; import com.intuit.karate.match.MatchType; @@ -68,19 +69,37 @@ public ScenarioEngine(Map vars, Logger logger) { this.logger = logger; } + // not in constructor because it has to be on Runable.run() thread public void init() { - JsEngine.remove(); // reset JS engine for this thread - JS = JsEngine.global(); + JS = JsEngine.local(); + } + + private void putJsBinding(String name, Variable v) { + switch (v.type) { + case JS_FUNCTION: + JsValue jv = v.getValue(); + // important to ensure that the function is attached to the current context + // since it may have come from e.g. karate-config.js or a calling / parent feature + // this will update the wrapper variable if needed as a performance optimization + jv.switchContext(JS); + JS.put(name, jv); + break; + case XML: + JS.put(name, XmlUtils.toObject(v.getValue())); + break; + default: + JS.put(name, v.getValue()); + } } public Variable eval(String exp) { - vars.forEach((k, v) -> JS.put(k, v.getValueForJsEngine())); + vars.forEach((k, v) -> putJsBinding(k, v)); return new Variable(JS.eval(exp)); } public void setHiddenVariable(String key, Object value) { if (value instanceof Variable) { - JS.put(key, ((Variable) value).getValue()); + putJsBinding(key, (Variable) value); } else { JS.put(key, value); } @@ -681,7 +700,7 @@ public MatchResult match(MatchType matchType, String expression, String path, St } } MatchValue actualValue = new MatchValue(actual.getValue()); - return Match.execute(matchType, actualValue, expectedValue); + return Match.execute(JS, matchType, actualValue, expectedValue); } private static final Pattern VAR_AND_PATH_PATTERN = Pattern.compile("\\w+"); @@ -774,22 +793,73 @@ public static StringUtils.Pair parseCallArgs(String line) { return new StringUtils.Pair(line.substring(0, pos), StringUtils.trimToNull(line.substring(pos))); } - public Variable call(boolean callOnce, String exp, boolean reuseParentConfig) { - StringUtils.Pair pair = parseCallArgs(exp); - Variable called = evalKarateExpression(pair.left); - Variable arg = pair.right == null ? null : evalKarateExpression(pair.right); + private Variable call(ScenarioRuntime runtime, Variable called, Variable arg, boolean reuseParentConfig) { switch (called.type) { case JAVA_FUNCTION: case JS_FUNCTION: return arg == null ? called.invokeFunction() : called.invokeFunction(new Object[]{arg.getValue()}); case KARATE_FEATURE: - 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 call(boolean callOnce, String exp, boolean reuseParentConfig) { + StringUtils.Pair pair = parseCallArgs(exp); + Variable called = evalKarateExpression(pair.left); + Variable arg = pair.right == null ? null : evalKarateExpression(pair.right); + ScenarioRuntime runtime = ScenarioRuntime.LOCAL.get(); + if (callOnce) { + return callOnce(runtime, exp, called, arg, reuseParentConfig); + } else { + return call(runtime, called, arg, reuseParentConfig); + } + } + + private Variable result(ScenarioRuntime runtime, ScenarioCall.Result result, boolean reuseParentConfig) { + if (reuseParentConfig) { // if shared scope + runtime.configure(new Config(result.config)); // re-apply config from time of snapshot + if (result.vars != null) { + vars.clear(); + vars.putAll(copyVariables(false)); // clone for safety + } + } + return result.value.copy(false); // clone result for safety + } + + private Variable callOnce(ScenarioRuntime runtime, String cacheKey, Variable called, Variable arg, boolean reuseParentConfig) { + // IMPORTANT: the call result is always shallow-cloned before returning + // so that call result (especially if a java Map) is not mutated by other scenarios + final Map CACHE = runtime.featureRuntime.FEATURE_CACHE; + ScenarioCall.Result result = CACHE.get(cacheKey); + if (result != null) { + runtime.logger.trace("callonce cache hit for: {}", cacheKey); + return result(runtime, result, reuseParentConfig); + } + long startTime = System.currentTimeMillis(); + runtime.logger.trace("callonce waiting for lock: {}", cacheKey); + synchronized (CACHE) { + result = CACHE.get(cacheKey); // retry + if (result != null) { + long endTime = System.currentTimeMillis() - startTime; + runtime.logger.warn("this thread waited {} milliseconds for callonce lock: {}", endTime, cacheKey); + return result(runtime, result, reuseParentConfig); + } + // this thread is the 'winner' + runtime.logger.info(">> lock acquired, begin callonce: {}", cacheKey); + Variable resultValue = call(runtime, called, arg, reuseParentConfig); + // we clone result (and config) here, to snapshot state at the point the callonce was invoked + // this prevents the state from being clobbered by the subsequent steps of this + // first scenario that is about to use the result + Map clonedVars = called.isKarateFeature() && reuseParentConfig ? copyVariables(false) : null; + result = new ScenarioCall.Result(resultValue.copy(false), new Config(runtime.getConfig()), clonedVars); + CACHE.put(cacheKey, result); + runtime.logger.info("<< lock released, cached callonce: {}", cacheKey); + return resultValue; // another routine will apply globally if needed + } + } + public Variable callFeature(ScenarioRuntime runtime, Feature feature, Variable arg, int index, boolean reuseParentConfig) { if (arg == null || arg.isMap()) { ScenarioCall call = new ScenarioCall(runtime, feature); 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 475968510..967858a95 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 @@ -391,6 +391,11 @@ public HttpRequest getPrevRequest() { public Config getConfig() { return config; } + + public void configure(Config config) { + this.config = config; + http = ScenarioHttpClient.construct(config); + } public void updateConfigCookies(Map cookies) { if (cookies == null) { 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 ca5d1063a..7b0cd5271 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 @@ -34,6 +34,8 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.w3c.dom.Node; /** @@ -42,6 +44,8 @@ */ public class Variable { + private static final Logger logger = LoggerFactory.getLogger(Variable.class); + public static enum Type { NULL, BOOLEAN, @@ -141,6 +145,10 @@ public boolean isOther() { public boolean isFunction() { return type == Type.JS_FUNCTION || type == Type.JAVA_FUNCTION; } + + public boolean isKarateFeature() { + return type == Type.KARATE_FEATURE; + } public boolean isTrue() { return type == Type.BOOLEAN && ((Boolean) value); @@ -234,15 +242,6 @@ public String getAsPrettyString() { } } - public Object getValueForJsEngine() { - switch (type) { - case XML: - return XmlUtils.toObject(getValue()); - default: - return value; - } - } - public int getAsInt() { if (isNumber()) { return ((Number) value).intValue(); @@ -254,17 +253,15 @@ public int getAsInt() { public Variable copy(boolean deep) { switch (type) { case LIST: - if (deep) { - return new Variable(JsonUtils.fromJsonString(getAsString())); - } else { - return new Variable(new ArrayList((List) value)); - } case MAP: if (deep) { - return new Variable(JsonUtils.fromJsonString(getAsString())); - } else { - return new Variable(new LinkedHashMap((Map) value)); + try { + return new Variable(JsonUtils.fromJsonString(getAsString())); + } catch (Throwable t) { + logger.warn("json deep clone failed, will fall-back to shallow: {}", t.getMessage()); + } } + return isMap() ? new Variable(new LinkedHashMap((Map) value)) : new Variable(new ArrayList((List) value)); case XML: return new Variable(XmlUtils.toXmlDoc(getAsString())); default: diff --git a/karate-core2/src/main/java/com/intuit/karate/template/TemplateEngineContext.java b/karate-core2/src/main/java/com/intuit/karate/template/TemplateEngineContext.java index 71e3b19a6..37dea9abd 100644 --- a/karate-core2/src/main/java/com/intuit/karate/template/TemplateEngineContext.java +++ b/karate-core2/src/main/java/com/intuit/karate/template/TemplateEngineContext.java @@ -65,7 +65,7 @@ public TemplateEngineContext(IEngineContext wrapped, RequestCycle requestCycle) private JsEngine LOCAL_ENGINE; public JsValue eval(boolean queue, String exp) { - JsEngine je = JsEngine.local(); + JsEngine je = JsEngine.localWithGlobalBindings(); requestCycle.setLocalEngine(je); Set variableNames = getVariableNames(); if (variableNames.contains(ENGINE)) { 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 e0023ff64..628122d0c 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 @@ -98,12 +98,22 @@ void testCallKarateFeature() { "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 }]"); + run( + "def b = 'bar'", + "def fun = function(i){ if (i == 1) return null; 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, index: 0, fun: '#ignore' }]"); + } + + @Test + void testCallOnce() { + run( + "def uuid = function(){ return java.util.UUID.randomUUID() + '' }", + "def first = callonce uuid", + "def second = callonce uuid" + ); + matchVarEquals("first", get("second")); } }