diff --git a/karate-core/src/main/java/com/intuit/karate/JsonUtils.java b/karate-core/src/main/java/com/intuit/karate/JsonUtils.java index 65e5d1c1f..b8e2536a3 100644 --- a/karate-core/src/main/java/com/intuit/karate/JsonUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/JsonUtils.java @@ -207,6 +207,16 @@ public static String toCsv(List> list) { return sw.toString(); } + public static Object shallowCopy(Object o) { + if (o instanceof List) { + return new ArrayList((List) o); + } else if (o instanceof Map) { + return new LinkedHashMap((Map) o); + } else { + return o; + } + } + public static Object deepCopy(Object o) { // anti recursion / back-references Set seen = Collections.newSetFromMap(new IdentityHashMap()); diff --git a/karate-core/src/main/java/com/intuit/karate/core/Config.java b/karate-core/src/main/java/com/intuit/karate/core/Config.java index 9cd2dd5b9..260a92a71 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/Config.java +++ b/karate-core/src/main/java/com/intuit/karate/core/Config.java @@ -27,11 +27,8 @@ import com.intuit.karate.StringUtils; import com.intuit.karate.driver.DockerTarget; import com.intuit.karate.driver.Target; -import com.intuit.karate.graal.JsEngine; -import com.intuit.karate.graal.JsFunction; import com.intuit.karate.http.Cookies; import com.intuit.karate.http.HttpLogModifier; -import org.graalvm.polyglot.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -110,38 +107,6 @@ public Config() { // zero arg constructor } - private static Variable attach(Variable v, JsEngine je) { - if (v.isJsFunctionWrapper()) { - JsFunction jf = v.getValue(); - Value attached = je.attachSource(jf.source); - return new Variable(attached); - } else { - return v; - } - } - - private static Variable detach(Variable v) { - if (v.isJsFunction()) { - return new Variable(new JsFunction(v.getValue())); - } else { - return v; - } - } - - protected void attach(JsEngine je) { - afterScenario = attach(afterScenario, je); - afterFeature = attach(afterFeature, je); - headers = attach(headers, je); - cookies = attach(cookies, je); - } - - protected void detach() { - afterScenario = detach(afterScenario); - afterFeature = detach(afterFeature); - headers = detach(headers); - cookies = detach(cookies); - } - private static T get(Map map, String key, T defaultValue) { Object o = map.get(key); return o == null ? defaultValue : (T) o; diff --git a/karate-core/src/main/java/com/intuit/karate/core/MockHandler.java b/karate-core/src/main/java/com/intuit/karate/core/MockHandler.java index de2d53ba0..c31e279b5 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/MockHandler.java +++ b/karate-core/src/main/java/com/intuit/karate/core/MockHandler.java @@ -117,7 +117,7 @@ public MockHandler(String prefix, List features, Map ar } } corsEnabled = corsEnabled || runtime.engine.getConfig().isCorsEnabled(); - globals.putAll(runtime.engine.detachVariables()); + globals.putAll(runtime.engine.shallowCloneVariables()); runtime.logger.info("mock server initialized: {}", feature); this.features.put(feature, runtime); } @@ -187,7 +187,7 @@ public synchronized Response handle(Request req) { // note the [synchronized] responseStatus = engine.vars.remove(ScenarioEngine.RESPONSE_STATUS); responseHeaders = engine.vars.remove(ScenarioEngine.RESPONSE_HEADERS); responseDelay = engine.vars.remove(RESPONSE_DELAY); - globals.putAll(engine.detachVariables()); + globals.putAll(engine.shallowCloneVariables()); Response res = new Response(200); if (result.isFailed()) { response = new Variable(result.getError().getMessage()); diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioBridge.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioBridge.java index dfbda5231..68e0c1707 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioBridge.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioBridge.java @@ -43,7 +43,6 @@ import com.intuit.karate.http.HttpRequest; import com.intuit.karate.http.HttpRequestBuilder; import com.intuit.karate.http.ResourceType; -import com.intuit.karate.http.Response; import com.intuit.karate.http.WebSocketClient; import com.intuit.karate.http.WebSocketOptions; import com.intuit.karate.shell.Command; @@ -180,9 +179,8 @@ private static Object callSingleResult(ScenarioEngine engine, Object o) throws E engine.logger.warn("callSingle() cached result is an exception"); throw (Exception) o; } - // if we don't clone, an attach operation would update the tree within the cached value - // causing future cache hit + attach attempts to fail ! - o = engine.recurseAndAttachAndShallowClone(o); + // shallow clone so that threads see the same data snapshot + o = JsonUtils.shallowCopy(o); return JsValue.fromJava(o); } @@ -257,8 +255,7 @@ public Object callSingle(String fileName, Value arg) throws Exception { engine.logger.warn("callSingleCache write failed, not json-like: {}", resultVar); } } - // functions have to be detached so that they can be re-hydrated in another js context - result = engine.recurseAndDetachAndShallowClone(resultVar.getValue()); + result = resultVar.getValue(); } CACHE.put(fileName, result); engine.logger.info("<< lock released, cached callSingle: {}", fileName); @@ -739,10 +736,10 @@ public Object repeat(int n, Value f) { } return new JsList(list); } - + public String responseHeader(String name) { return getEngine().getResponse().getHeader(name); - } + } // set multiple variables in one shot public void set(Map map) { @@ -879,12 +876,7 @@ public String toCsv(Object o) { } public Object toJava(Value value) { - if (value.canExecute()) { - JsEngine copy = getEngine().JS.copy(); - return new JsLambda(copy.attach(value)); - } else { - return new JsValue(value).getValue(); - } + return new JsValue(value).getValue(); } public File toJavaFile(String path) { @@ -980,8 +972,7 @@ public WebSocketClient webSocket(String url, Value listener, Value value) { if (listener == null || !listener.canExecute()) { handler = m -> true; } else { - JsEngine copy = engine.JS.copy(); - handler = new JsLambda(copy.attach(listener)); + handler = new JsLambda(listener); } WebSocketOptions options = new WebSocketOptions(url, value == null ? null : new JsValue(value).getValue()); options.setTextHandler(handler); @@ -1002,8 +993,7 @@ public WebSocketClient webSocketBinary(String url, Value listener, Value value) if (listener == null || !listener.canExecute()) { handler = m -> true; } else { - JsEngine copy = engine.JS.copy(); - handler = new JsLambda(copy.attach(listener)); + handler = new JsLambda(listener); } WebSocketOptions options = new WebSocketOptions(url, value == null ? null : new JsValue(value).getValue()); options.setBinaryHandler(handler); diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioEngine.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioEngine.java index f354f72c4..40d6f3ca5 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioEngine.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioEngine.java @@ -36,8 +36,6 @@ import com.intuit.karate.driver.DriverOptions; import com.intuit.karate.driver.Key; import com.intuit.karate.graal.JsEngine; -import com.intuit.karate.graal.JsExecutable; -import com.intuit.karate.graal.JsFunction; import com.intuit.karate.graal.JsLambda; import com.intuit.karate.graal.JsValue; import com.intuit.karate.http.*; @@ -64,6 +62,7 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.graalvm.polyglot.proxy.ProxyExecutable; import org.slf4j.LoggerFactory; /** @@ -305,7 +304,6 @@ public Config getConfig() { // callonce routine is one example public void setConfig(Config config) { this.config = config; - config.attach(JS); if (requestBuilder != null) { requestBuilder.client.setConfig(config); } @@ -1034,30 +1032,12 @@ protected String docInternal(Map options) { public void init() { // not in constructor because it has to be on Runnable.run() thread JS = JsEngine.local(); logger.trace("js context: {}", JS); - // to avoid re-processing objects that have cyclic dependencies - Set seen = Collections.newSetFromMap(new IdentityHashMap()); - runtime.magicVariables.forEach((k, v) -> { - // even hidden variables may need pre-processing - // for e.g. the __arg may contain functions that originated in a different js context - Object o = recurseAndAttach(k, v, seen); - JS.put(k, o == null ? v : o); // attach returns null if "not dirty" - }); - vars.forEach((k, v) -> { - // re-hydrate any functions from caller or background - Object o = recurseAndAttach(k, v.getValue(), seen); - // note that we don't update the vars ! - // if we do, any "bad" out-of-context values will crash the constructor of Variable - // it is possible the vars are detached / re-used later, so we kind of defer the inevitable - JS.put(k, o == null ? v.getValue() : o); // attach returns null if "not dirty" - }); + runtime.magicVariables.forEach((k, v) -> JS.put(k, v)); + vars.forEach((k, v) -> JS.put(k, v.getValue())); if (runtime.caller.arg != null && runtime.caller.arg.isMap()) { // add the call arg as separate "over ride" variables Map arg = runtime.caller.arg.getValue(); - recurseAndAttach("", arg, seen); // since arg is a map, it will not be cloned - arg.forEach((k, v) -> { - vars.put(k, new Variable(v)); - JS.put(k, v); - }); + setVariables(arg); } JS.put(KARATE, bridge); JS.put(READ, readFunction); @@ -1079,228 +1059,10 @@ public void init() { // not in constructor because it has to be on Runnable.run( } } - protected Map detachVariables() { - Set seen = Collections.newSetFromMap(new IdentityHashMap()); - Map detached = new HashMap(vars.size()); - vars.forEach((k, v) -> { - Object o = recurseAndDetachAndShallowClone(k, v.getValue(), seen); - detached.put(k, new Variable(o)); - }); - return detached; - } - - // callSingle - protected Object recurseAndAttachAndShallowClone(Object o) { - return recurseAndAttachAndShallowClone(o, Collections.newSetFromMap(new IdentityHashMap())); - } - - // callonce - protected Object recurseAndAttachAndShallowClone(Object o, Set seen) { - if (o instanceof List) { - o = new ArrayList((List) o); - } else if (o instanceof Map) { - o = new LinkedHashMap((Map) o); - } - Object result = recurseAndAttach("", o, seen); - return result == null ? o : result; - } - - // call shared context - protected Object recurseAndAttach(Object o) { - Object result = recurseAndAttach("", o, Collections.newSetFromMap(new IdentityHashMap())); - return result == null ? o : result; - } - - // call shared context - protected Object shallowClone(Object o) { - if (o instanceof List) { - return this.shallowCloneList((List) o); - } else if (o instanceof Map) { - return this.shallowCloneMap((Map) o); - } else { - return o; - } - } - - // call shared context - protected List shallowCloneList(List o) { - List result = new ArrayList(); - o.forEach(v -> { - if (v instanceof List) { - List copy = new ArrayList(); - copy.addAll((List) v); - result.add(copy); - } else if (v instanceof Map) { - Map copy = new HashMap(); - copy.putAll((Map) v); - result.add(copy); - } else { - result.add(v); - } - }); - return result; - } - - // call shared context - protected Map shallowCloneMap(Map o) { - Map result = new HashMap(); - o.forEach((k, v) -> { - if (v instanceof List) { - List copy = new ArrayList(); - copy.addAll((List) v); - result.put(k, copy); - } else if (v instanceof Map) { - Map copy = new HashMap(); - copy.putAll((Map) v); - result.put(k, copy); - } else { - result.put(k, v); - } - }); - return result; - } - - private Object recurseAndAttach(String name, Object o, Set seen) { - if (o instanceof Value) { - Value value = Value.asValue(o); - try { - if (value.canExecute()) { - if (value.isMetaObject()) { // js function - return attach(value); - } else { // java function - return new JsExecutable(value); - } - } else { // anything else, including java-type references - return value; - } - } catch (Exception e) { - logger.warn("[*** attach ***] ignoring non-json value: '{}' - {}", name, e.getMessage()); - // here we try our luck and hope that graal does not notice ! - return value; - } - } - if (o instanceof Class) { - Class clazz = (Class) o; - Value value = JS.evalForValue("Java.type('" + clazz.getCanonicalName() + "')"); - return value; - } else if (o instanceof JsFunction) { - JsFunction jf = (JsFunction) o; - try { - return attachSource(jf.source); - } catch (Exception e) { - logger.warn("[*** attach ***] ignoring js-function: '{}' - {}", name, e.getMessage()); - return Value.asValue(null); // make sure we return a "dirty" value to force an update - } - } else if (o instanceof List) { - if (seen.add(o)) { - List list = (List) o; - int count = list.size(); - try { - for (int i = 0; i < count; i++) { - Object child = list.get(i); - Object childResult = recurseAndAttach(name + "[" + i + "]", child, seen); - if (childResult != null) { - list.set(i, childResult); - } - } - } catch (Exception e) { - logger.warn("attach - immutable list: {}", name); - } - } - return null; - } else if (o instanceof Map) { - if (seen.add(o)) { - Map map = (Map) o; - try { - map.forEach((k, v) -> { - Object childResult = recurseAndAttach(name + "." + k, v, seen); - if (childResult != null) { - map.put(k, childResult); - } - }); - } catch (Exception e) { - logger.warn("attach - immutable map: {}", name); - } - } - return null; - } else { - return null; - } - } - - protected Object recurseAndDetachAndShallowClone(Object o) { - return recurseAndDetachAndShallowClone("", o, Collections.newSetFromMap(new IdentityHashMap())); - } - - // callonce, callSingle and detachVariables() - private Object recurseAndDetachAndShallowClone(String name, Object o, Set seen) { - if (o instanceof List) { - o = new ArrayList((List) o); - } else if (o instanceof Map) { - o = new LinkedHashMap((Map) o); - } - Object result = recurseAndDetach(name, o, seen); - return result == null ? o : result; - } - - private Object recurseAndDetach(String name, Object o, Set seen) { - if (o instanceof Value) { - Value value = (Value) o; - try { - if (value.canExecute()) { - if (value.isMetaObject()) { // js function - return new JsFunction(value); - } else { // java function - return new JsExecutable(value); - } - } else if (value.isHostObject()) { - return value.asHostObject(); - } - } catch (Exception e) { - logger.warn("[*** detach ***] ignoring non-json value: '{}' - {}", name, e.getMessage()); - } - return null; - } else if (o instanceof List) { - List list = (List) o; - int count = list.size(); - try { - for (int i = 0; i < count; i++) { - Object child = list.get(i); - Object childResult = recurseAndDetach(name + "[" + i + "]", child, seen); - if (childResult != null) { - list.set(i, childResult); - } - } - } catch (Exception e) { - logger.warn("detach - immutable list: {}", name); - } - return null; - } else if (o instanceof Map) { - if (seen.add(o)) { - Map map = (Map) o; - try { - map.forEach((k, v) -> { - Object childResult = recurseAndDetach(name + "." + k, v, seen); - if (childResult != null) { - map.put(k, childResult); - } - }); - } catch (Exception e) { - logger.warn("detach - immutable map: {}", name); - } - } - return null; - } else { - return null; - } - } - - public Value attachSource(CharSequence source) { - return JS.attachSource(source); - } - - public Value attach(Value before) { - return JS.attach(before); + protected Map shallowCloneVariables() { + Map copy = new HashMap(vars.size()); + vars.forEach((k, v) -> copy.put(k, v.copy(false))); // shallow clone + return copy; } protected Map getOrEvalAsMap(Variable var, Object... args) { @@ -1315,9 +1077,9 @@ protected Map 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(JS.attach(jsFunction), args); - return new Variable(jsResult); + ProxyExecutable pe = var.getValue(); + Object result = JsEngine.execute(pe, args); + return new Variable(result); case JAVA_FUNCTION: // definitely a "call" with a single argument Function javaFunction = var.getValue(); Object arg = args.length == 0 ? null : args[0]; @@ -1328,17 +1090,6 @@ public Variable executeFunction(Variable var, Object... args) { } } - private JsValue executeJsValue(Value function, Object... args) { - try { - return new JsValue(JsEngine.execute(function, args)); - } catch (Exception e) { - String jsSource = function.getSourceLocation().getCharacters().toString(); - KarateException ke = JsEngine.fromJsEvalException(jsSource, e, null); - setFailedReason(ke); - throw ke; - } - } - public Variable evalJs(String js) { try { return new Variable(JS.eval(js)); @@ -1368,16 +1119,9 @@ public void setVariable(String key, Object value) { o = v.getValue(); } else { o = value; - try { - v = new Variable(value); - } catch (Exception e) { - v = null; - logger.warn("[*** set variable ***] ignoring non-json value: {} - {}", key, e.getMessage()); - } - } - if (v != null) { - vars.put(key, v); + v = new Variable(value); } + vars.put(key, v); if (JS != null) { JS.put(key, o); } @@ -1446,6 +1190,10 @@ private static boolean isEmbeddedExpression(String text) { return text != null && (text.startsWith("#(") || text.startsWith("##(")) && text.endsWith(")"); } + private Map Map(Object callResult) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + private static class EmbedAction { final boolean remove; @@ -1994,6 +1742,7 @@ public static StringUtils.Pair parseCallArgs(String line) { } return new StringUtils.Pair(line.substring(0, pos), StringUtils.trimToNull(line.substring(pos))); } + public Variable call(Variable called, Variable arg, boolean sharedScope) { switch (called.type) { @@ -2003,36 +1752,13 @@ 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); - this.rehydrateCallFeatureResult(callResult); + // 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>(); - final List> finalCallResultVariables = (List>)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 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()); @@ -2064,11 +1790,11 @@ public Variable call(boolean callOnce, String exp, boolean sharedScope) { Variable resultVariables = this.getCallFeatureVariables(result); if (sharedScope) { if (resultVariables.isMap()) { - setVariables(resultVariables.getValue()); - } else if (resultVariables.isList()) { - ((List) resultVariables.getValue()).forEach(r -> { - setVariables((Map) r); - }); + // even the act of introspecting graal values as part of the JsValue constructor + // triggers the dreaded graal js single-thread check, so we lock here + synchronized (JsValue.LOCK) { + setVariables(resultVariables.getValue()); + } } if (result.getValue() instanceof FeatureResult) { setConfig(((FeatureResult) result.getValue()).getConfig()); @@ -2081,40 +1807,26 @@ private Variable callOnceResult(ScenarioCall.Result result, boolean sharedScope) if (sharedScope) { // if shared scope vars.clear(); // clean slate if (result.vars != null) { - Set seen = Collections.newSetFromMap(new IdentityHashMap()); - result.vars.forEach((k, v) -> { - // clone maps and lists so that subsequent steps don't modify data / references being passed around - Object o = recurseAndAttachAndShallowClone(v.getValue(), seen); - try { - vars.put(k, new Variable(o)); - } catch (Exception e) { - logger.warn("[*** callonce result ***] ignoring non-json value: '{}' - {}", k, e.getMessage()); - } - }); + // shallow clone maps and lists so that subsequent steps don't modify data / references being passed around + result.vars.forEach((k, v) -> vars.put(k, v.copy(false))); } 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()); - } - }); + Map map = result.value.getValue(); + // shallow clone newly added variables + map.forEach((k, v) -> vars.put(k, new Variable(JsonUtils.shallowCopy(v)))); } else { - logger.warn("[*** callonce result ***] ignoring non-map value from result.value: {}", result.value); + logger.warn("callonce: ignoring non-map value from result.value: {}", result.value); } } - init(); // this will attach and also insert magic variables + init(); // this will insert magic variables // re-apply config from time of snapshot - // and note that setConfig() will attach functions such as configured "headers" setConfig(new Config(result.config)); return Variable.NULL; // since we already reset the vars above we return null // else the call() routine would try to do it again // note that shared scope means a return value is meaningless } else { - // deep-clone for the same reasons mentioned above - Object resultValue = recurseAndAttachAndShallowClone(result.value.getValue()); - return new Variable(resultValue); + // shallow clone for the same reasons mentioned above + return result.value.copy(false); } } @@ -2144,17 +1856,11 @@ private Variable callOnce(String cacheKey, Variable called, Variable arg, boolea 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 clonedVars = called.isFeature() && sharedScope ? detachVariables() : null; - Config clonedConfig = new Config(config); - clonedConfig.detach(); - Object resultObject = recurseAndDetachAndShallowClone(resultVariables.getValue()); - result = new ScenarioCall.Result(new Variable(resultObject), clonedConfig, clonedVars); + Map clonedVars = called.isFeature() && sharedScope ? shallowCloneVariables() : null; + result = new ScenarioCall.Result(resultVariables.copy(false), new Config(config), clonedVars); CACHE.put(cacheKey, result); logger.info("<< lock released, cached callonce: {}", cacheKey); // another routine will apply globally if needed - // wrap and attach if being used immediately in a Scenario return callOnceResult(result, sharedScope); } } @@ -2170,13 +1876,6 @@ public Object callFeature(Feature feature, Variable arg, int index, boolean shar THREAD_LOCAL.set(this); FeatureResult result = fr.result; runtime.addCallResult(result); - if (sharedScope) { - // if it's shared scope we don't want JS functions rehydrated in different contexts (threads) - // 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; diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioIterator.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioIterator.java index 581db24ea..36111a15b 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioIterator.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioIterator.java @@ -93,7 +93,6 @@ public boolean tryAdvance(Consumer 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; diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioRuntime.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioRuntime.java index 00c1047ff..244b18379 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioRuntime.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioRuntime.java @@ -39,6 +39,7 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -78,7 +79,6 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, Scenari logAppender = new StringLogAppender(false); if (background != null) { config = new Config(background.engine.getConfig()); - config.detach(); } else { config = new Config(); } @@ -90,17 +90,16 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, Scenari } else if (caller.isSharedScope()) { logAppender = caller.parentRuntime.logAppender; ScenarioEngine parentEngine = background == null ? caller.parentRuntime.engine : background.engine; - Config config = parentEngine.getConfig(); - config.detach(); Map vars = caller.parentRuntime.engine.vars; - engine = new ScenarioEngine(config, this, vars, logger, parentEngine.requestBuilder.copy(null)); + engine = new ScenarioEngine(parentEngine.getConfig(), this, vars, logger, parentEngine.requestBuilder.copy(null)); } else { // new, but clone and copy data logAppender = caller.parentRuntime.logAppender; 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, parentEngine.requestBuilder.copy(null)); + // in this case, parent variables are set via magic variables - see initMagicVariables() + // which means the variables are only in the JS engine - [ see ScenarioEngine.init() ] + // and not "visible" via ScenarioEngine constructor (vars) + // one consequence is that they won't show up in the debug variables view + engine = new ScenarioEngine(new Config(parentEngine.getConfig()), this, new HashMap(), logger, parentEngine.requestBuilder.copy(null)); } logger.setAppender(logAppender); actions = new ScenarioActions(engine); @@ -114,8 +113,8 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, Scenari engine.requestBuilder = background.engine.requestBuilder.copy(client); } result.addStepResults(background.result.getStepResults()); - Map detached = background.engine.detachVariables(); - detached.forEach((k, v) -> engine.vars.put(k, v)); + Map copy = background.engine.shallowCloneVariables(); + copy.forEach((k, v) -> engine.vars.put(k, v)); } dryRun = featureRuntime.suite.dryRun; tags = scenario.getTagsEffective(); @@ -281,18 +280,12 @@ private Map initMagicVariables() { // karate principle: parent variables are always "visible" // so we inject the parent variables // but they will be over-written by what is local to this scenario - - if (caller.isSharedScope()) { - map.putAll(caller.parentRuntime.magicVariables); - } else { - // the shallow clone of variables is important - // otherwise graal / js functions in calling context get corrupted - caller.parentRuntime.engine.vars.forEach((k, v) -> map.put(k, v == null ? null : v.copy(false).getValue())); - - // shallow copy magicVariables - map.putAll((Map) caller.parentRuntime.engine.shallowClone(caller.parentRuntime.magicVariables)); + if (!caller.isSharedScope()) { + // shallow clone variables if not shared scope + Map copy = caller.parentRuntime.engine.shallowCloneVariables(); + copy.forEach((k, v) -> map.put(k, v.getValue())); } - + map.putAll(caller.parentRuntime.magicVariables); map.put("__arg", caller.arg == null ? null : caller.arg.getValue()); map.put("__loop", caller.getLoopIndex()); } @@ -379,8 +372,7 @@ public void beforeRun() { } ScenarioEngine.set(engine); engine.init(); - engine.getConfig().attach(engine.JS); - if (this.background != null) { + if (background != null) { ScenarioEngine backgroundEngine = background.engine; if (backgroundEngine.driver != null) { engine.setDriver(backgroundEngine.driver); @@ -509,7 +501,6 @@ public StepResult execute(Step step) { } else { stopped = true; } - if (stopped && (!this.engine.getConfig().isContinueAfterContinueOnStepFailure() || !this.engine.isIgnoringStepErrors())) { error = stepResult.getError(); logError(error.getMessage()); diff --git a/karate-core/src/main/java/com/intuit/karate/core/Variable.java b/karate-core/src/main/java/com/intuit/karate/core/Variable.java index 6029654d4..72fea77f7 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/Variable.java +++ b/karate-core/src/main/java/com/intuit/karate/core/Variable.java @@ -28,13 +28,13 @@ import com.intuit.karate.graal.JsValue; import com.intuit.karate.Json; import com.intuit.karate.JsonUtils; -import com.intuit.karate.graal.JsFunction; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyExecutable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; @@ -76,13 +76,10 @@ public Variable(Object o) { } if (o == null) { type = Type.NULL; + } else if (o instanceof ProxyExecutable) { + type = Type.JS_FUNCTION; } else if (o instanceof Value) { - Value v = (Value) o; - if (v.canExecute()) { - type = Type.JS_FUNCTION; - } else { - type = Type.OTHER; // java.lang.Class - } + type = Type.OTHER; // java.lang.Class } else if (o instanceof Function) { type = Type.JAVA_FUNCTION; } else if (o instanceof Node) { @@ -123,10 +120,6 @@ public boolean isJsFunction() { return type == Type.JS_FUNCTION; } - public boolean isJsFunctionWrapper() { - return value instanceof JsFunction; - } - public boolean isBytes() { return type == Type.BYTES; } diff --git a/karate-core/src/main/java/com/intuit/karate/graal/JsEngine.java b/karate-core/src/main/java/com/intuit/karate/graal/JsEngine.java index 35d136806..084338de0 100644 --- a/karate-core/src/main/java/com/intuit/karate/graal/JsEngine.java +++ b/karate-core/src/main/java/com/intuit/karate/graal/JsEngine.java @@ -36,6 +36,7 @@ import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyExecutable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -115,9 +116,6 @@ public JsEngine copy() { Value v = bindings.getMember(key); if (v.isHostObject()) { temp.bindings.putMember(key, v); - } else if (v.canExecute()) { - Value fun = temp.evalForValue("(" + v.getSourceLocation().getCharacters() + ")"); - temp.bindings.putMember(key, fun); } else { temp.bindings.putMember(key, JsValue.toJava(v)); } @@ -160,21 +158,14 @@ public JsValue get(String key) { } return new JsValue(value); } - - public Value attachSource(CharSequence source) { - Value value = evalForValue("(" + source + ")"); - return attach(value); - } - - public Value attach(Value function) { - try { - return context.asValue(function); - } catch (Exception e) { - logger.trace("context switch: {}", e.getMessage()); - CharSequence source = function.getSourceLocation().getCharacters(); - return evalForValue("(" + source + ")"); + + public static Object execute(ProxyExecutable function, Object... args) { + Value[] values = new Value[args.length]; + for (int i = 0; i < args.length; i++) { + values[i] = Value.asValue(args[i]); } - } + return function.execute(values); + } public static Value execute(Value function, Object... args) { for (int i = 0; i < args.length; i++) { diff --git a/karate-core/src/main/java/com/intuit/karate/graal/JsExecutable.java b/karate-core/src/main/java/com/intuit/karate/graal/JsExecutable.java deleted file mode 100644 index e435c80ed..000000000 --- a/karate-core/src/main/java/com/intuit/karate/graal/JsExecutable.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * The MIT License - * - * Copyright 2021 Intuit 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.graal; - -import org.graalvm.polyglot.Value; -import org.graalvm.polyglot.proxy.ProxyExecutable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * @author pthomas3 - */ -public class JsExecutable implements ProxyExecutable { - - private static final Logger logger = LoggerFactory.getLogger(JsExecutable.class); - - public final Value value; - - public JsExecutable(Value value) { - this.value = value; - } - - private static final Object LOCK = new Object(); - - protected static Value invoke(Value value, Object... args) { - try { - return JsEngine.execute(value, args); - } catch (Exception e) { - logger.warn("[*** execute ***] invocation failed: {}", e.getMessage()); - synchronized (LOCK) { - return JsEngine.execute(value, args); - } - } - } - - @Override - public Object execute(Value... values) { - Object[] args = new Object[values.length]; - for (int i = 0; i < args.length; i++) { - args[i] = values[i].as(Object.class); - } - return invoke(value, args); - } - -} diff --git a/karate-core/src/main/java/com/intuit/karate/graal/JsFunction.java b/karate-core/src/main/java/com/intuit/karate/graal/JsFunction.java deleted file mode 100644 index eeb2049fc..000000000 --- a/karate-core/src/main/java/com/intuit/karate/graal/JsFunction.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * The MIT License - * - * Copyright 2020 Intuit 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.graal; - -import org.graalvm.polyglot.Value; - -/** - * - * @author pthomas3 - */ -public class JsFunction { - - public final Value value; - public final CharSequence source; - - public JsFunction(Value value) { - this.value = value; - source = value.getSourceLocation().getCharacters(); - } - -} diff --git a/karate-core/src/main/java/com/intuit/karate/graal/JsLambda.java b/karate-core/src/main/java/com/intuit/karate/graal/JsLambda.java index b7e81c288..7258d83c9 100644 --- a/karate-core/src/main/java/com/intuit/karate/graal/JsLambda.java +++ b/karate-core/src/main/java/com/intuit/karate/graal/JsLambda.java @@ -23,6 +23,7 @@ */ package com.intuit.karate.graal; +import com.intuit.karate.graal.JsValue.SharableMembersAndInstantiable; import java.util.function.Consumer; import java.util.function.Function; import org.graalvm.polyglot.Value; @@ -33,30 +34,33 @@ * * @author pthomas3 */ -public class JsLambda implements Consumer, Function, Runnable { +public class JsLambda extends SharableMembersAndInstantiable implements Consumer, Function, Runnable { private static final Logger logger = LoggerFactory.getLogger(JsLambda.class); - public final Value value; - - public JsLambda(Value value) { - this.value = value; + public JsLambda(Value v) { + super(v); } @Override public void accept(Object arg) { - JsExecutable.invoke(value, arg); + synchronized (JsValue.LOCK) { + JsEngine.execute(this, arg); + } } @Override public Object apply(Object arg) { - Value res = JsExecutable.invoke(value, arg); - return JsValue.toJava(res); + synchronized (JsValue.LOCK) { + return JsEngine.execute(this, arg); + } } @Override public void run() { - JsExecutable.invoke(value); + synchronized (JsValue.LOCK) { + JsEngine.execute(this); + } } } diff --git a/karate-core/src/main/java/com/intuit/karate/graal/JsValue.java b/karate-core/src/main/java/com/intuit/karate/graal/JsValue.java index f70bb5b6c..7eb824eeb 100644 --- a/karate-core/src/main/java/com/intuit/karate/graal/JsValue.java +++ b/karate-core/src/main/java/com/intuit/karate/graal/JsValue.java @@ -35,6 +35,9 @@ import java.util.function.Function; import org.graalvm.polyglot.Value; import org.graalvm.polyglot.proxy.Proxy; +import org.graalvm.polyglot.proxy.ProxyExecutable; +import org.graalvm.polyglot.proxy.ProxyInstantiable; +import org.graalvm.polyglot.proxy.ProxyObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; @@ -55,7 +58,7 @@ public static enum Type { NULL, OTHER } - + public static final JsValue NULL = new JsValue(Value.asValue(null)); private final Value original; @@ -68,9 +71,16 @@ public JsValue(Value v) { } this.original = v; try { - if (v.isNull()) { // apparently this can be a "host object" as well ! + if (v.isNull()) { value = null; type = Type.NULL; + } else if (v.isHostObject()) { + if (v.isMetaObject()) { // java.lang.Class ! + value = v; // special case, keep around as graal value + } else { + value = v.asHostObject(); + } + type = Type.OTHER; } else if (v.isProxyObject()) { Object o = v.asProxyObject(); if (o instanceof JsXml) { @@ -82,27 +92,13 @@ public JsValue(Value v) { } else if (o instanceof JsList) { value = ((JsList) o).getList(); type = Type.ARRAY; - } else if (o instanceof JsExecutable) { - value = (JsExecutable) o; - type = Type.FUNCTION; + } else if (o instanceof ProxyExecutable) { + value = o; + type = Type.FUNCTION; } else { // e.g. custom bridge, e.g. Request value = v.as(Object.class); type = Type.OTHER; } - } else if (v.isHostObject()) { // java object - if (v.isMetaObject()) { // java.lang.Class ! - value = v; // special case, keep around as graal value - } else { - value = v.asHostObject(); - } - type = Type.OTHER; - } else if (v.canExecute()) { - if (v.isMetaObject()) { // js function - value = v; // special case, keep around as graal value - } else { // java function reference - value = new JsExecutable(v); - } - type = Type.FUNCTION; } else if (v.hasArrayElements()) { int size = (int) v.getArraySize(); List list = new ArrayList(size); @@ -113,17 +109,40 @@ public JsValue(Value v) { value = list; type = Type.ARRAY; } else if (v.hasMembers()) { - Set keys = v.getMemberKeys(); - Map map = new LinkedHashMap(keys.size()); - for (String key : keys) { - Value child = v.getMember(key); - map.put(key, new JsValue(child).value); + if (v.canExecute()) { + if (v.canInstantiate()) { + // js functions have members, can be executed and are instantiable + value = new SharableMembersAndInstantiable(v); + } else { + value = new SharableMembersAndExecutable(v); + } + type = Type.FUNCTION; + } else { + Set keys = v.getMemberKeys(); + Map map = new LinkedHashMap(keys.size()); + for (String key : keys) { + Value child = v.getMember(key); + map.put(key, new JsValue(child).value); + } + value = map; + type = Type.OBJECT; } - value = map; - type = Type.OBJECT; + } else if (v.isNumber()) { + value = v.as(Number.class); + type = Type.OTHER; + } else if (v.isBoolean()) { + value = v.asBoolean(); + type = Type.OTHER; + } else if (v.isString()) { + value = v.asString(); + type = Type.OTHER; } else { value = v.as(Object.class); - type = Type.OTHER; + if (value instanceof Function) { + type = Type.FUNCTION; + } else { + type = Type.OTHER; + } } } catch (Exception e) { if (logger.isTraceEnabled()) { @@ -184,7 +203,7 @@ public boolean isOther() { public String toString() { return original.toString(); } - + public String toJsonOrXmlString(boolean pretty) { return toString(value, pretty); } @@ -195,7 +214,7 @@ public String getAsString() { public static Object fromJava(Object o) { if (o instanceof Function || o instanceof Proxy) { - return o; + return o; } else if (o instanceof List) { return new JsList((List) o); } else if (o instanceof Map) { @@ -226,7 +245,7 @@ public static Object unWrap(Object o) { public static byte[] toBytes(Value v) { return toBytes(toJava(v)); } - + public static String toString(Object o) { return toString(o, false); } @@ -316,7 +335,7 @@ public static Object fromStringSafe(String raw) { return raw; } } - + public static boolean isTruthy(Object o) { if (o == null) { return false; @@ -329,5 +348,82 @@ public static boolean isTruthy(Object o) { } return true; } - + + static class SharableMembers implements ProxyObject { + + final Value v; + + SharableMembers(Value v) { + this.v = v; + } + + @Override + public void putMember(String key, Value value) { + v.putMember(key, new JsValue(value).value); + } + + @Override + public boolean hasMember(String key) { + return v.hasMember(key); + } + + @Override + public Object getMemberKeys() { + return v.getMemberKeys().toArray(new String[0]); + } + + @Override + public Object getMember(String key) { + return new JsValue(v.getMember(key)).value; + } + + @Override + public boolean removeMember(String key) { + return v.removeMember(key); + } + } + + public static final Object LOCK = new Object(); + + static class SharableMembersAndExecutable extends SharableMembers implements ProxyExecutable { + + SharableMembersAndExecutable(Value v) { + super(v); + } + + @Override + public Object execute(Value... args) { + Object[] newArgs = new Object[args.length]; + // the synchronized block should include the pre-processing of arguments + synchronized (LOCK) { + for (int i = 0; i < newArgs.length; i++) { + newArgs[i] = new JsValue(args[i]).value; + } + Value result = v.execute(newArgs); + return new JsValue(result).value; + } + } + + } + + static class SharableMembersAndInstantiable extends SharableMembersAndExecutable implements ProxyInstantiable { + + SharableMembersAndInstantiable(Value v) { + super(v); + } + + @Override + public Object newInstance(Value... args) { + Object[] newArgs = new Object[args.length]; + // the synchronized block should include the pre-processing of arguments + synchronized (LOCK) { + for (int i = 0; i < newArgs.length; i++) { + newArgs[i] = new JsValue(args[i]).value; + } + return new JsValue(v.execute(newArgs)).value; + } + } + + } + } diff --git a/karate-core/src/test/java/com/intuit/karate/core/ScenarioRuntimeTest.java b/karate-core/src/test/java/com/intuit/karate/core/ScenarioRuntimeTest.java index 534d2ed4c..19786407e 100644 --- a/karate-core/src/test/java/com/intuit/karate/core/ScenarioRuntimeTest.java +++ b/karate-core/src/test/java/com/intuit/karate/core/ScenarioRuntimeTest.java @@ -607,7 +607,7 @@ void testCallJsFunctionShared() { "def myFn = function(x){ return { myVar: x } }", "call myFn 'foo'" ); - assertEquals(get("myVar"), "foo"); + assertEquals(get("myVar"), "foo"); } @Test diff --git a/karate-core/src/test/java/com/intuit/karate/core/VariableTest.java b/karate-core/src/test/java/com/intuit/karate/core/VariableTest.java index 2b782ec2e..ce2ad97e3 100644 --- a/karate-core/src/test/java/com/intuit/karate/core/VariableTest.java +++ b/karate-core/src/test/java/com/intuit/karate/core/VariableTest.java @@ -38,8 +38,6 @@ void testJsFunction() { Variable var = new Variable(jv); assertTrue(var.isJsFunction()); assertFalse(var.isJavaFunction()); - JsValue res = new JsValue(JsEngine.execute(var.getValue(), new Object[]{1, 2})); - assertEquals(3, res.getValue()); } @Test @@ -71,8 +69,7 @@ void testJavaFunction() { assertTrue(v.isJavaFunction()); v = new Variable((BiFunction) this::simpleBiFunction); // maybe we are ok with this, karate "call" can be used only with functions - assertFalse(v.isJavaFunction()); - + assertFalse(v.isJavaFunction()); } } diff --git a/karate-core/src/test/java/com/intuit/karate/core/jscall/js-call.feature b/karate-core/src/test/java/com/intuit/karate/core/jscall/js-call.feature index 442f03a10..5d77a66cd 100644 --- a/karate-core/src/test/java/com/intuit/karate/core/jscall/js-call.feature +++ b/karate-core/src/test/java/com/intuit/karate/core/jscall/js-call.feature @@ -3,5 +3,4 @@ Feature: Scenario: * def result = karate.call('classpath:com/intuit/karate/core/jscall/dummy.feature') * utils.sayHello() -# TODO broke after graal upgrade to 22 -# * karate.call('js-called.feature') \ No newline at end of file +* karate.call('js-called.feature') \ No newline at end of file diff --git a/karate-core/src/test/java/com/intuit/karate/core/parallel/HelloTest.java b/karate-core/src/test/java/com/intuit/karate/core/parallel/HelloTest.java new file mode 100644 index 000000000..dad17216a --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parallel/HelloTest.java @@ -0,0 +1,39 @@ +package com.intuit.karate.core.parallel; + +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import com.intuit.karate.core.Feature; +import com.intuit.karate.core.MockHandler; +import com.intuit.karate.http.HttpServer; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +class HelloTest { + + static final Logger logger = LoggerFactory.getLogger(HelloTest.class); + + static HttpServer server; + + @BeforeAll + static void beforeAll() { + MockHandler mock = new MockHandler(Feature.read("classpath:com/intuit/karate/core/parallel/mock.feature")); + server = HttpServer.handler(mock).build(); + } + + @Test + void testParallel() { + Results results = Runner.path("classpath:com/intuit/karate/core/parallel/hello.feature") + .configDir("classpath:com/intuit/karate/core/parallel") + .systemProperty("server.port", server.getPort() + "") + .parallel(3); + assertEquals(0, results.getFailCount(), results.getErrorMessages()); + } + +} diff --git a/karate-core/src/test/java/com/intuit/karate/core/parallel/call-from-config3.feature b/karate-core/src/test/java/com/intuit/karate/core/parallel/call-from-config3.feature deleted file mode 100644 index e2be1e6c5..000000000 --- a/karate-core/src/test/java/com/intuit/karate/core/parallel/call-from-config3.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: -Background: - * url 'http://localhost:' + karate.properties['server.port'] - -Scenario: reproducing #1835 - # https://github.com/karatelabs/karate/issues/1835#issuecomment-969471445 - * call read('parallel-outline-call-api.feature') [{'key':'value'}, {'key':'value2'}] - * def headers = response.headers - * call read('parallel-outline-call-api.feature') - * def headers = response.headers - * def r = call read('parallel-outline-call-api.feature') - * def headers = r.response.headers \ No newline at end of file diff --git a/karate-core/src/test/java/com/intuit/karate/core/parallel/hello.feature b/karate-core/src/test/java/com/intuit/karate/core/parallel/hello.feature new file mode 100644 index 000000000..78731cdd0 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parallel/hello.feature @@ -0,0 +1,22 @@ +Feature: + +Background: +* url serverUrl +* callonce read('call-once-from-feature.feature') + +Scenario: 1 +* match HelloOnce.sayHello('world') == 'hello world' + +Scenario: 2 +* match HelloOnce.sayHello('world') == 'hello world' + +Scenario: 3 +* match HelloOnce.sayHello('world') == 'hello world' + +Scenario: N +* call sayHelloOnce 'three' + +* path 'three' +* method get +* status 200 +* match response == { three: '#string' } diff --git a/karate-core/src/test/java/com/intuit/karate/core/parallel/karate-config.js b/karate-core/src/test/java/com/intuit/karate/core/parallel/karate-config.js index 2ae120e4e..25f157ba6 100644 --- a/karate-core/src/test/java/com/intuit/karate/core/parallel/karate-config.js +++ b/karate-core/src/test/java/com/intuit/karate/core/parallel/karate-config.js @@ -10,7 +10,5 @@ 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; } diff --git a/karate-core/src/test/java/com/intuit/karate/core/parasimple/ParallelOutlineSimpleTest.java b/karate-core/src/test/java/com/intuit/karate/core/parasimple/ParallelOutlineSimpleTest.java new file mode 100644 index 000000000..d55e8e98a --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parasimple/ParallelOutlineSimpleTest.java @@ -0,0 +1,38 @@ +package com.intuit.karate.core.parasimple; + +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import com.intuit.karate.core.Feature; +import com.intuit.karate.core.MockHandler; +import com.intuit.karate.http.HttpServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ParallelOutlineSimpleTest { + + static final Logger logger = LoggerFactory.getLogger(ParallelOutlineSimpleTest.class); + + static HttpServer server; + + @BeforeAll + static void beforeAll() { + MockHandler mock = new MockHandler(Feature.read("classpath:com/intuit/karate/core/parallel/mock.feature")); + server = HttpServer.handler(mock).build(); + } + + @Test + void testParallelOutline() { + Results results = Runner.path( + "classpath:com/intuit/karate/core/parasimple/parallel-outline-simple.feature") + .configDir("classpath:com/intuit/karate/core/parasimple") + .systemProperty("server.port", server.getPort() + "") + .parallel(3); + assertEquals(0, results.getFeaturesFailed()); + assertEquals(0, results.getScenariosFailed()); + assertEquals(0, results.getFailCount()); + } + +} diff --git a/karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.feature b/karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.feature new file mode 100644 index 000000000..1f3d85d3f --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.feature @@ -0,0 +1,4 @@ +Feature: + +Scenario: +* configure headers = read('headers.js') diff --git a/karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.js b/karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.js new file mode 100644 index 000000000..6098494f6 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.js @@ -0,0 +1,3 @@ +function fn() { + return {}; +} diff --git a/karate-core/src/test/java/com/intuit/karate/core/parasimple/karate-config.js b/karate-core/src/test/java/com/intuit/karate/core/parasimple/karate-config.js new file mode 100644 index 000000000..6098494f6 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parasimple/karate-config.js @@ -0,0 +1,3 @@ +function fn() { + return {}; +} diff --git a/karate-core/src/test/java/com/intuit/karate/core/parasimple/parallel-outline-simple.feature b/karate-core/src/test/java/com/intuit/karate/core/parasimple/parallel-outline-simple.feature new file mode 100644 index 000000000..58f2fc049 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/core/parasimple/parallel-outline-simple.feature @@ -0,0 +1,16 @@ +Feature: + +Background: + * def data = [{ name: 'value1' }, { name: 'value2' }, { name: 'value3' }, { name: 'value4' }] + * call read('headers.feature') + +Scenario Outline: + * url 'http://localhost:' + karate.properties['server.port'] + * path 'fromfeature' + * method get + * status 200 + * match response == { message: 'from feature' } + + Examples: + | data | +