From 251efee843ff3a1d6f380d25c8de088d545389b6 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Sat, 6 Aug 2022 16:54:22 +0530 Subject: [PATCH] wip cleanup of runtime life cycle #1905 --- .../intuit/karate/core/FeatureRuntime.java | 2 +- .../com/intuit/karate/core/MockHandler.java | 104 +++++++++--------- .../com/intuit/karate/core/ScenarioCall.java | 8 +- .../intuit/karate/core/ScenarioEngine.java | 5 - .../intuit/karate/core/ScenarioIterator.java | 20 ++-- .../intuit/karate/core/ScenarioOutline.java | 3 +- .../intuit/karate/core/ScenarioRuntime.java | 52 ++++----- 7 files changed, 93 insertions(+), 101 deletions(-) diff --git a/karate-core/src/main/java/com/intuit/karate/core/FeatureRuntime.java b/karate-core/src/main/java/com/intuit/karate/core/FeatureRuntime.java index 1ca7a8c32..8eb7503ef 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/FeatureRuntime.java +++ b/karate-core/src/main/java/com/intuit/karate/core/FeatureRuntime.java @@ -79,7 +79,7 @@ public void setMockEngine(ScenarioEngine mockEngine) { public ScenarioEngine getMockEngine() { return mockEngine; - } + } public static FeatureRuntime forTempUse(HttpClientFactory hcf) { Suite sr = Suite.forTempUse(hcf); 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 66193c07a..3e59373c6 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 @@ -91,39 +91,22 @@ public MockHandler(List features) { public MockHandler(String prefix, List features, Map args) { this.prefix = "/".equals(prefix) ? null : prefix; - for (Feature feature : features) { - FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(HttpClientFactory.DEFAULT), feature, args); - FeatureSection section = new FeatureSection(); - section.setIndex(-1); // TODO util for creating dummy scenario - Scenario dummy = new Scenario(feature, section, -1); - section.setScenario(dummy); - ScenarioRuntime runtime = new ScenarioRuntime(featureRuntime, dummy); - initRuntime(runtime); - if (feature.isBackgroundPresent()) { - // if we are within a scenario already e.g. karate.start(), preserve context - ScenarioEngine prevEngine = ScenarioEngine.get(); - try { - ScenarioEngine.set(runtime.engine); - for (Step step : feature.getBackground().getSteps()) { - Result result = StepRuntime.execute(step, runtime.actions); - if (result.isFailed()) { - String message = "mock-server background failed - " + feature + ":" + step.getLine(); - runtime.logger.error(message); - throw new KarateException(message, result.getError()); - } - } - } finally { - ScenarioEngine.set(prevEngine); - } - } + features.forEach(feature -> { + ScenarioRuntime runtime = initRuntime(feature, args); corsEnabled = corsEnabled || runtime.engine.getConfig().isCorsEnabled(); globals.putAll(runtime.engine.shallowCloneVariables()); runtime.logger.info("mock server initialized: {}", feature); - featureRuntimes.put(feature, runtime); - } + featureRuntimes.put(feature, runtime); + }); } - private void initRuntime(ScenarioRuntime runtime) { + private ScenarioRuntime initRuntime(Feature feature, Map args) { + FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(HttpClientFactory.DEFAULT), feature, args); + FeatureSection section = new FeatureSection(); + section.setIndex(-1); // TODO util for creating dummy scenario + Scenario dummy = new Scenario(feature, section, -1); + section.setScenario(dummy); + ScenarioRuntime runtime = new ScenarioRuntime(featureRuntime, dummy); runtime.engine.setVariable(PATH_MATCHES, (Function) this::pathMatches); runtime.engine.setVariable(PARAM_EXISTS, (Function) this::paramExists); runtime.engine.setVariable(PARAM_VALUE, (Function) this::paramValue); @@ -133,6 +116,24 @@ private void initRuntime(ScenarioRuntime runtime) { runtime.engine.setVariable(HEADER_CONTAINS, (BiFunction) this::headerContains); runtime.engine.setVariable(BODY_PATH, (Function) this::bodyPath); runtime.engine.init(); + if (feature.isBackgroundPresent()) { + // if we are within a scenario already e.g. karate.start(), preserve context + ScenarioEngine prevEngine = ScenarioEngine.get(); + try { + ScenarioEngine.set(runtime.engine); + for (Step step : feature.getBackground().getSteps()) { + Result result = StepRuntime.execute(step, runtime.actions); + if (result.isFailed()) { + String message = "mock-server background failed - " + feature + ":" + step.getLine(); + runtime.logger.error(message); + throw new KarateException(message, result.getError()); + } + } + } finally { + ScenarioEngine.set(prevEngine); + } + } + return runtime; } private static final Result PASSED = Result.passed(0); @@ -161,27 +162,10 @@ public synchronized Response handle(Request req) { // note the [synchronized] Feature feature = entry.getKey(); ScenarioRuntime runtime = entry.getValue(); // important for graal to work properly - Thread.currentThread().setContextClassLoader(runtime.featureRuntime.suite.classLoader); - // begin init engine for this request + Thread.currentThread().setContextClassLoader(runtime.featureRuntime.suite.classLoader); LOCAL_REQUEST.set(req); req.processBody(); - ScenarioEngine engine = new ScenarioEngine(runtime, new HashMap<>(globals)); - engine.init(); - engine.setVariable(ScenarioEngine.REQUEST_URL_BASE, req.getUrlBase()); - engine.setVariable(ScenarioEngine.REQUEST_URI, req.getPath()); - engine.setVariable(ScenarioEngine.REQUEST_METHOD, req.getMethod()); - engine.setVariable(ScenarioEngine.REQUEST_HEADERS, req.getHeaders()); - engine.setVariable(ScenarioEngine.REQUEST, req.getBodyConverted()); - engine.setVariable(REQUEST_PARAMS, req.getParams()); - engine.setVariable(REQUEST_BYTES, req.getBody()); - engine.setRequest(req); - runtime.featureRuntime.setMockEngine(engine); - ScenarioEngine.set(engine); - Map>> parts = req.getMultiParts(); - if (parts != null) { - engine.setHiddenVariable(REQUEST_PARTS, parts); - } - // end init engine for this request + ScenarioEngine engine = initEngine(runtime, globals, req); for (FeatureSection fs : feature.getSections()) { if (fs.isOutline()) { runtime.logger.warn("skipping scenario outline - {}:{}", feature, fs.getScenarioOutline().getLine()); @@ -192,8 +176,7 @@ public synchronized Response handle(Request req) { // note the [synchronized] Map configureHeaders; Variable response, responseStatus, responseHeaders, responseDelay; ScenarioActions actions = new ScenarioActions(engine); - Result result = PASSED; - result = executeScenarioSteps(feature, runtime, scenario, actions, result); + Result result = executeScenarioSteps(feature, runtime, scenario, actions); engine.mockAfterScenario(); configureHeaders = engine.mockConfigureHeaders(); response = engine.vars.remove(ScenarioEngine.RESPONSE); @@ -242,8 +225,29 @@ public synchronized Response handle(Request req) { // note the [synchronized] } return new Response(404); } + + private static ScenarioEngine initEngine(ScenarioRuntime runtime, Map globals, Request req) { + ScenarioEngine engine = new ScenarioEngine(runtime.engine.getConfig(), runtime, new HashMap(globals), runtime.logger); + engine.init(); + engine.setVariable(ScenarioEngine.REQUEST_URL_BASE, req.getUrlBase()); + engine.setVariable(ScenarioEngine.REQUEST_URI, req.getPath()); + engine.setVariable(ScenarioEngine.REQUEST_METHOD, req.getMethod()); + engine.setVariable(ScenarioEngine.REQUEST_HEADERS, req.getHeaders()); + engine.setVariable(ScenarioEngine.REQUEST, req.getBodyConverted()); + engine.setVariable(REQUEST_PARAMS, req.getParams()); + engine.setVariable(REQUEST_BYTES, req.getBody()); + engine.setRequest(req); + runtime.featureRuntime.setMockEngine(engine); + ScenarioEngine.set(engine); + Map>> parts = req.getMultiParts(); + if (parts != null) { + engine.setHiddenVariable(REQUEST_PARTS, parts); + } + return engine; + } - private Result executeScenarioSteps(Feature feature, ScenarioRuntime runtime, Scenario scenario, ScenarioActions actions, Result result) { + private Result executeScenarioSteps(Feature feature, ScenarioRuntime runtime, Scenario scenario, ScenarioActions actions) { + Result result = PASSED; for (Step step : scenario.getSteps()) { result = StepRuntime.execute(step, actions); if (result.isAborted()) { diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioCall.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioCall.java index db9dd9fe2..00417e86c 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioCall.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioCall.java @@ -23,7 +23,6 @@ */ package com.intuit.karate.core; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -67,11 +66,8 @@ public Config getParentConfig(boolean copy) { if (parentRuntime == null) { return new Config(); } - if (copy) { - return new Config(parentRuntime.engine.getConfig()); - } else { - return parentRuntime.engine.getConfig(); - } + Config temp = parentRuntime.engine.getConfig(); + return copy ? new Config(temp) : temp; } public boolean isNone() { 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 2ae110709..d1e875dd2 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 @@ -111,11 +111,6 @@ public class ScenarioEngine { protected JsEngine JS; - // only used by mock server - public ScenarioEngine(ScenarioRuntime runtime, Map vars) { - this(runtime.engine.config, runtime, vars, runtime.logger); - } - public ScenarioEngine(Config config, ScenarioRuntime runtime, Map vars, Logger logger) { this.config = config; this.runtime = runtime; 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 fa13b1773..1f623a907 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 @@ -23,7 +23,6 @@ */ package com.intuit.karate.core; -import com.intuit.karate.graal.JsEngine; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -47,6 +46,7 @@ public class ScenarioIterator implements Spliterator { private Scenario currentScenario; // dynamic + private ScenarioRuntime dynamicRuntime; private Variable expressionValue; private int index; @@ -90,8 +90,11 @@ public boolean tryAdvance(Consumer action) { if (currentScenario.isDynamic()) { if (expressionValue == null) { String expression = currentScenario.getDynamicExpression(); + dynamicRuntime = new ScenarioRuntime(featureRuntime, currentScenario); try { - expressionValue = new Variable(JsEngine.evalGlobal(expression)); + ScenarioEngine.set(dynamicRuntime.engine); + dynamicRuntime.engine.init(); + expressionValue = dynamicRuntime.engine.evalJs(expression); if (expressionValue.isList() || expressionValue.isJsOrJavaFunction()) { // all good } else { @@ -99,10 +102,9 @@ public boolean tryAdvance(Consumer action) { } } catch (Exception e) { String message = "dynamic expression evaluation failed: " + expression; - ScenarioRuntime dummy = new ScenarioRuntime(featureRuntime, currentScenario); - dummy.result.addFakeStepResult(message, e); + dynamicRuntime.result.addFakeStepResult(message, e); currentScenario = null; - action.accept(dummy); + action.accept(dynamicRuntime); return true; // exit early } } @@ -110,13 +112,13 @@ public boolean tryAdvance(Consumer action) { Variable rowValue; if (expressionValue.isJsOrJavaFunction()) { try { - rowValue = ScenarioEngine.get().executeFunction(expressionValue, rowIndex); + ScenarioEngine.set(dynamicRuntime.engine); + rowValue = dynamicRuntime.engine.executeFunction(expressionValue, rowIndex); } catch (Exception e) { String message = "dynamic function expression evaluation failed at index " + rowIndex + ": " + e.getMessage(); - ScenarioRuntime dummy = new ScenarioRuntime(featureRuntime, currentScenario); - dummy.result.addFakeStepResult(message, e); + dynamicRuntime.result.addFakeStepResult(message, e); currentScenario = null; - action.accept(dummy); + action.accept(dynamicRuntime); return true; // exit early } } else { // is list diff --git a/karate-core/src/main/java/com/intuit/karate/core/ScenarioOutline.java b/karate-core/src/main/java/com/intuit/karate/core/ScenarioOutline.java index 1966f9ef9..11f02edb5 100644 --- a/karate-core/src/main/java/com/intuit/karate/core/ScenarioOutline.java +++ b/karate-core/src/main/java/com/intuit/karate/core/ScenarioOutline.java @@ -101,8 +101,7 @@ public List getScenarios(FeatureRuntime fr) { if (selectedForExecution) { Table table = examples.getTable(); if (table.isDynamic()) { - // technically row index 0 to denote an example (not -1) - Scenario scenario = toScenario(table.getDynamicExpression(), 0, table.getLineNumberForRow(0), examples.getTags()); + Scenario scenario = toScenario(table.getDynamicExpression(), -1, table.getLineNumberForRow(0), examples.getTags()); list.add(scenario); } else { int rowCount = table.getRows().size(); 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 12962a44b..83a80be9b 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 @@ -68,32 +68,23 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario) { this.caller = featureRuntime.caller; perfMode = featureRuntime.perfHook != null; if (caller.isNone()) { - Config config; logAppender = new StringLogAppender(false); - config = new Config(); - engine = new ScenarioEngine(config, this, new HashMap(), logger); + engine = new ScenarioEngine(caller.getParentConfig(false), this, new HashMap(), logger); } else if (caller.isSharedScope()) { logAppender = caller.parentRuntime.logAppender; - Map vars = caller.getParentVars(false); - Config config = caller.getParentConfig(false); - engine = new ScenarioEngine(config, this, vars, logger); + engine = new ScenarioEngine(caller.getParentConfig(false), this, caller.getParentVars(false), logger); } else { // new, but clone and copy data logAppender = caller.parentRuntime.logAppender; - Config config = caller.parentRuntime.engine.getConfig(); // 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 - // and more importantly don't get passed back to caller and float around, bloating memory - engine = new ScenarioEngine(new Config(config), this, new HashMap(), logger); + engine = new ScenarioEngine(caller.getParentConfig(true), this, new HashMap(), logger); } logger.setAppender(logAppender); actions = new ScenarioActions(engine); this.scenario = scenario; - if (scenario.isDynamic() && !scenario.isOutlineExample()) { // dynamic scenario iterator error + if (scenario.isDynamic() && !scenario.isOutlineExample()) { // from dynamic scenario iterator steps = Collections.emptyList(); skipped = true; // ensures run() is a no-op - magicVariables = null; + magicVariables = Collections.emptyMap(); } else { magicVariables = initMagicVariables(); } @@ -105,6 +96,10 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario) { } private Map initMagicVariables() { + // magic variables are only in the JS engine - [ see ScenarioEngine.init() ] + // and not "visible" and tracked in ScenarioEngine.vars + // one consequence is that they won't show up in the debug variables view + // but more importantly don't get passed back to caller and float around, bloating memory Map map = new HashMap(); if (!caller.isNone()) { // karate principle: parent variables are always "visible" @@ -297,43 +292,44 @@ private void evalConfigJs(String js, String displayName) { } private static boolean isSelectedForExecution(FeatureRuntime fr, Scenario scenario, Tags tags) { + org.slf4j.Logger logger = FeatureRuntime.logger; Feature feature = scenario.getFeature(); int callLine = feature.getCallLine(); if (callLine != -1) { int sectionLine = scenario.getSection().getLine(); int scenarioLine = scenario.getLine(); if (callLine == sectionLine || callLine == scenarioLine) { - fr.logger.info("found scenario at line: {}", callLine); + logger.info("found scenario at line: {}", callLine); return true; } - fr.logger.trace("skipping scenario at line: {}, needed: {}", scenario.getLine(), callLine); + logger.trace("skipping scenario at line: {}, needed: {}", scenario.getLine(), callLine); return false; } String callName = feature.getCallName(); if (callName != null) { if (scenario.getName().matches(callName)) { - fr.logger.info("found scenario at line: {} - {}", scenario.getLine(), callName); + logger.info("found scenario at line: {} - {}", scenario.getLine(), callName); return true; } - fr.logger.trace("skipping scenario at line: {} - {}, needed: {}", scenario.getLine(), scenario.getName(), callName); + logger.trace("skipping scenario at line: {} - {}, needed: {}", scenario.getLine(), scenario.getName(), callName); return false; } String callTag = feature.getCallTag(); if (callTag != null && (!fr.caller.isNone() || fr.perfHook != null)) { // only if this is a legit "call" or a gatling "call by tag" if (tags.contains(callTag)) { - fr.logger.info("{} - call by tag at line {}: {}", fr, scenario.getLine(), callTag); + logger.info("{} - call by tag at line {}: {}", fr, scenario.getLine(), callTag); return true; } - fr.logger.trace("skipping scenario at line: {} with call by tag effective: {}", scenario.getLine(), callTag); + logger.trace("skipping scenario at line: {} with call by tag effective: {}", scenario.getLine(), callTag); return false; } if (fr.caller.isNone()) { if (tags.evaluate(fr.suite.tagSelector, fr.suite.env)) { - fr.logger.trace("matched scenario at line: {} with tags effective: {}", scenario.getLine(), tags.getTags()); + logger.trace("matched scenario at line: {} with tags effective: {}", scenario.getLine(), tags.getTags()); return true; } - fr.logger.trace("skipping scenario at line: {} with tags effective: {}", scenario.getLine(), tags.getTags()); + logger.trace("skipping scenario at line: {} with tags effective: {}", scenario.getLine(), tags.getTags()); return false; } else { return true; // when called, tags are ignored, all scenarios will be run @@ -434,14 +430,14 @@ public StepResult execute(Step step) { stopped = true; logger.debug("abort at {}", step.getDebugInfo()); } else if (stepResult.isFailed()) { - if (stepResult.getMatchingMethod() != null && this.engine.getConfig().getContinueOnStepFailureMethods().contains(stepResult.getMatchingMethod().method)) { + if (stepResult.getMatchingMethod() != null && engine.getConfig().getContinueOnStepFailureMethods().contains(stepResult.getMatchingMethod().method)) { stopped = false; ignoringFailureSteps = true; currentStepResult.setErrorIgnored(true); } else { stopped = true; } - if (stopped && (!this.engine.getConfig().isContinueAfterContinueOnStepFailure() || !this.engine.isIgnoringStepErrors())) { + if (stopped && (!this.engine.getConfig().isContinueAfterContinueOnStepFailure() || !engine.isIgnoringStepErrors())) { error = stepResult.getError(); logError(error.getMessage()); } @@ -451,12 +447,12 @@ public StepResult execute(Step step) { } addStepLogEmbedsAndCallResults(); if (currentStepResult.isErrorIgnored()) { - this.engine.setFailedReason(null); + engine.setFailedReason(null); } - if (!this.engine.isIgnoringStepErrors() && this.isIgnoringFailureSteps()) { - if (this.engine.getConfig().isContinueAfterContinueOnStepFailure()) { + if (!engine.isIgnoringStepErrors() && isIgnoringFailureSteps()) { + if (engine.getConfig().isContinueAfterContinueOnStepFailure()) { // continue execution and reset failed reason for engine to null - this.engine.setFailedReason(null); + engine.setFailedReason(null); ignoringFailureSteps = false; } else { // stop execution