From 3002f9a4dfd7fcac85a8df88c6977a60b2315e6a Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Wed, 18 Jul 2018 21:24:20 +0530 Subject: [PATCH 01/23] fix for gatling report missing last scenario http request #460 --- .../java/com/intuit/karate/cucumber/AsyncScenario.java | 10 ++++++++++ karate-gatling/src/test/resources/karate-config.js | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 karate-gatling/src/test/resources/karate-config.js diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncScenario.java b/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncScenario.java index 19113dda6..82275fa41 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncScenario.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncScenario.java @@ -43,6 +43,13 @@ public AsyncScenario(ScenarioWrapper scenario, KarateBackend backend) { this.backend = backend; this.iterator = scenario.getSteps().iterator(); } + + private void afterScenario() { + StepInterceptor interceptor = backend.getCallContext().stepInterceptor; + if (interceptor != null) { + interceptor.afterScenario(scenario, backend); + } + } @Override public void submit(Consumer system, BiConsumer next) { @@ -52,8 +59,10 @@ public void submit(Consumer system, BiConsumer { if (r != null && r.isAbort()) { + afterScenario(); next.accept(null, null); // abort: exit early } else if (e != null) { + afterScenario(); next.accept(null, e); // exit with error } else { AsyncScenario.this.submit(system, next); @@ -61,6 +70,7 @@ public void submit(Consumer system, BiConsumer Date: Wed, 18 Jul 2018 21:24:46 +0530 Subject: [PATCH 02/23] bump dev version to point one --- karate-apache/pom.xml | 2 +- karate-archetype/pom.xml | 2 +- karate-core/pom.xml | 2 +- karate-demo/pom.xml | 2 +- karate-gatling/pom.xml | 2 +- karate-jersey/pom.xml | 2 +- karate-junit4/pom.xml | 2 +- karate-mock-servlet/pom.xml | 2 +- karate-netty/pom.xml | 2 +- karate-restdocs/pom.xml | 2 +- karate-testng/pom.xml | 2 +- karate-web/pom.xml | 2 +- pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/karate-apache/pom.xml b/karate-apache/pom.xml index e06ece2d3..ca96df4fd 100755 --- a/karate-apache/pom.xml +++ b/karate-apache/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-apache jar diff --git a/karate-archetype/pom.xml b/karate-archetype/pom.xml index 10c33e65b..803a73bf1 100755 --- a/karate-archetype/pom.xml +++ b/karate-archetype/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-archetype jar diff --git a/karate-core/pom.xml b/karate-core/pom.xml index 396871726..1ae626656 100755 --- a/karate-core/pom.xml +++ b/karate-core/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-core jar diff --git a/karate-demo/pom.xml b/karate-demo/pom.xml index bd15e25c7..b616ae2fd 100755 --- a/karate-demo/pom.xml +++ b/karate-demo/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-demo diff --git a/karate-gatling/pom.xml b/karate-gatling/pom.xml index 482ad9ac8..d1831dc4d 100644 --- a/karate-gatling/pom.xml +++ b/karate-gatling/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-gatling jar diff --git a/karate-jersey/pom.xml b/karate-jersey/pom.xml index 5c313b351..fb5523512 100755 --- a/karate-jersey/pom.xml +++ b/karate-jersey/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-jersey jar diff --git a/karate-junit4/pom.xml b/karate-junit4/pom.xml index 9d053a0c6..f05730406 100755 --- a/karate-junit4/pom.xml +++ b/karate-junit4/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-junit4 jar diff --git a/karate-mock-servlet/pom.xml b/karate-mock-servlet/pom.xml index 63ea1fdf7..a80c67770 100644 --- a/karate-mock-servlet/pom.xml +++ b/karate-mock-servlet/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-mock-servlet diff --git a/karate-netty/pom.xml b/karate-netty/pom.xml index 703e25620..0faecce7d 100644 --- a/karate-netty/pom.xml +++ b/karate-netty/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-netty jar diff --git a/karate-restdocs/pom.xml b/karate-restdocs/pom.xml index 849dcaf2a..2bae93f5d 100644 --- a/karate-restdocs/pom.xml +++ b/karate-restdocs/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-restdocs jar diff --git a/karate-testng/pom.xml b/karate-testng/pom.xml index cc31089ad..a23dfd40d 100755 --- a/karate-testng/pom.xml +++ b/karate-testng/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-testng jar diff --git a/karate-web/pom.xml b/karate-web/pom.xml index 2554ea75e..dab437d8d 100644 --- a/karate-web/pom.xml +++ b/karate-web/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 karate-web diff --git a/pom.xml b/pom.xml index 357fbc2d1..2abbfc0e4 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.intuit.karate karate-parent - 0.8.0 + 0.8.1 pom ${project.artifactId} From 9d6f26c13ca9a563cda1a65955df273feed455f1 Mon Sep 17 00:00:00 2001 From: Venu Madhav Chukkapalli Date: Sat, 21 Jul 2018 01:34:52 +0530 Subject: [PATCH 03/23] Issue-464: proper handling of abort and slight improvement in KarateJunitFormatter --- .../karate/cucumber/KarateJunitFormatter.java | 4 +- .../intuit/karate/cucumber/KarateRuntime.java | 17 ++-- .../karate/cucumber/FeatureResultTest.java | 91 +++++++++++++++++++ .../intuit/karate/cucumber/aborted.feature | 16 ++++ .../com/intuit/karate/cucumber/failed.feature | 16 ++++ 5 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 karate-core/src/test/java/com/intuit/karate/cucumber/FeatureResultTest.java create mode 100644 karate-core/src/test/java/com/intuit/karate/cucumber/aborted.feature create mode 100644 karate-core/src/test/java/com/intuit/karate/cucumber/failed.feature diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java index f417ed29f..5f3054bad 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java @@ -216,6 +216,8 @@ public void examples(Examples examples) { public void endOfScenarioLifeCycle(Scenario scenario) { if (testCase.steps.isEmpty()) { testCase.handleEmptyTestCase(doc, currentElement); + } else { + testCase.updateElement(currentElement); } } @@ -232,7 +234,7 @@ private void addDummyTestCase() { @Override public void result(Result result) { testCase.results.add(result); - testCase.updateElement(currentElement); + //testCase.updateElement(currentElement); } private double sumTimes(NodeList testCaseNodes) { diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java index 9dbc5844a..7371ac370 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java @@ -52,7 +52,6 @@ public class KarateRuntime extends Runtime { private final CucumberStats stats; private CucumberScenarioImpl scenarioResult; private boolean stopped; - private boolean aborted; private ScriptContext prevContext; public KarateRuntime(KarateRuntimeOptions kro, KarateBackend backend, RuntimeGlue glue) { @@ -74,13 +73,20 @@ private void addStepToCounterAndResult(Result result) { public void runStep(String featurePath, Step step, Reporter reporter, I18n i18n) { if (stopped) { Match match = Match.UNDEFINED; - Result result = aborted ? StepResult.PASSED : Result.SKIPPED; - if (reporter instanceof KarateReporter) { // simulate cucumber flow to keep json-formatter happy + Result result = Result.SKIPPED; + if (reporter instanceof KarateReporter) { // simulate cucumber flow to keep json-formatter happy + // @pthomas3^^ (please review and post your comment on this change) + // below call internally invokes reporter.match(match) and reporter.result(result) as + // KarateReporterBase.karateStep() -> karateStepProceed() -> result() / match() + // causing double invocation of reporter.match(match) and reporter.result(result) + // because they were invoked below this if. To avoid this, we should moved them to else block + // TODO: remove this comment (meant to get clarification from pthomas3) before merging the PR ((KarateReporter) reporter).karateStep(step, match, result, backend.getCallContext(), backend.getStepDefs().getContext()); + } else { + reporter.match(match); + reporter.result(result); } - reporter.match(match); addStepToCounterAndResult(result); - reporter.result(result); return; } StepResult result = CucumberUtils.runStep(step, reporter, i18n, backend); @@ -91,7 +97,6 @@ public void runStep(String featurePath, Step step, Reporter reporter, I18n i18n) } prevContext = backend.getStepDefs().getContext(); stopped = true; // skip remaining steps - aborted = result.isAbort(); // if skipped steps are to be marked as PASSED } addStepToCounterAndResult(result.getResult()); } diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureResultTest.java b/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureResultTest.java new file mode 100644 index 000000000..797780f29 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureResultTest.java @@ -0,0 +1,91 @@ +/* + * The MIT License + * + * Copyright 2017 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.cucumber; + +import com.intuit.karate.FileUtils; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author vmchukky + */ +public class FeatureResultTest { + + private static final Logger logger = LoggerFactory.getLogger(FeatureResultTest.class); + + @Test + public void testFailureMultiScenarioFeature() throws Exception { + String reportPath = "target/failed-feature-result.xml"; + File file = new File("src/test/java/com/intuit/karate/cucumber/failed.feature"); + KarateJunitAndJsonReporter reporter = CucumberRunnerTest.run(file, reportPath); + KarateJunitFormatter formatter = reporter.getJunitFormatter(); + assertEquals(2, formatter.getFailCount()); + assertEquals(3, formatter.getTestCount()); + String contents = FileUtils.toString(new File(reportPath)); + assertTrue(contents.contains("assert evaluated to false: a != 1")); + assertTrue(contents.contains("assert evaluated to false: a == 3")); + + // failure1 should have first step as failure, and second step as skipped + // TODO: generate the expected content string, below code puts a hard dependency + // with KarateJunitFormatter$TestCase.addStepAndResultListing() + assertTrue(contents.contains("Then assert a != 1..........................................................failed")); + assertTrue(contents.contains("And assert a == 2...........................................................skipped")); + + // failure2 should have first step as passed, and second step as failed + assertTrue(contents.contains("Then assert a != 2..........................................................passed")); + assertTrue(contents.contains("And assert a == 3...........................................................failed")); + + // pass1 should have both steps as passed + assertTrue(contents.contains("Then assert a != 4..........................................................passed")); + assertTrue(contents.contains("And assert a != 5...........................................................passed")); + } + + @Test + public void testAbortMultiScenarioFeature() throws Exception { + String reportPath = "target/aborted-feature-result.xml"; + File file = new File("src/test/java/com/intuit/karate/cucumber/aborted.feature"); + KarateJunitAndJsonReporter reporter = CucumberRunnerTest.run(file, reportPath); + KarateJunitFormatter formatter = reporter.getJunitFormatter(); + assertEquals(0, formatter.getFailCount()); + assertEquals(3, formatter.getTestCount()); + String contents = FileUtils.toString(new File(reportPath)); + + // skip-pass and skip-fail both should have all steps as skipped + // TODO: generate the expected content string, below code puts a hard dependency + // with KarateJunitFormatter$TestCase.addStepAndResultListing() + assertTrue(contents.contains("* eval karate.abort().......................................................skipped")); + assertTrue(contents.contains("* assert a == 1.............................................................skipped")); + assertTrue(contents.contains("* assert a == 2.............................................................skipped")); + + // noskip should have both steps as passed + assertTrue(contents.contains("Then assert a != 3..........................................................passed")); + assertTrue(contents.contains("And assert a != 4...........................................................passed")); + } +} diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/aborted.feature b/karate-core/src/test/java/com/intuit/karate/cucumber/aborted.feature new file mode 100644 index 000000000..4cef6a63d --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/aborted.feature @@ -0,0 +1,16 @@ +@ignore +Feature: abort2pass1 +Background: +Given def a = 1 + +Scenario: skip-pass +* eval karate.abort() +* assert a == 1 + +Scenario: skip-fail +* eval karate.abort() +* assert a == 2 + +Scenario: noskip +Then assert a != 3 +And assert a != 4 diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/failed.feature b/karate-core/src/test/java/com/intuit/karate/cucumber/failed.feature new file mode 100644 index 000000000..8f6f1e72f --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/failed.feature @@ -0,0 +1,16 @@ +@ignore +Feature: fail2pass1 +Background: +Given def a = 1 + +Scenario: failure1 +Then assert a != 1 +And assert a == 2 + +Scenario: failure2 +Then assert a != 2 +And assert a == 3 + +Scenario: pass1 +Then assert a != 4 +And assert a != 5 From 0224e783512f7738695b17b943e23a278dbc1b27 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Mon, 23 Jul 2018 12:14:41 +0530 Subject: [PATCH 04/23] code doc edit after pr #466 thx @vmchukky --- .../main/java/com/intuit/karate/cucumber/KarateRuntime.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java index 7371ac370..e20b62af2 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateRuntime.java @@ -75,12 +75,9 @@ public void runStep(String featurePath, Step step, Reporter reporter, I18n i18n) Match match = Match.UNDEFINED; Result result = Result.SKIPPED; if (reporter instanceof KarateReporter) { // simulate cucumber flow to keep json-formatter happy - // @pthomas3^^ (please review and post your comment on this change) // below call internally invokes reporter.match(match) and reporter.result(result) as // KarateReporterBase.karateStep() -> karateStepProceed() -> result() / match() - // causing double invocation of reporter.match(match) and reporter.result(result) - // because they were invoked below this if. To avoid this, we should moved them to else block - // TODO: remove this comment (meant to get clarification from pthomas3) before merging the PR + // the else clause is needed to compensate ! ((KarateReporter) reporter).karateStep(step, match, result, backend.getCallContext(), backend.getStepDefs().getContext()); } else { reporter.match(match); From 19264eaac7b3a708a3342012a7366b5da360d389 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Wed, 25 Jul 2018 00:25:18 +0530 Subject: [PATCH 05/23] karate ui was broken with logging change ref #469 --- .../src/main/java/com/intuit/karate/ui/AppSession.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java index 30f3655af..24a7cf4f9 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java @@ -42,8 +42,6 @@ import java.util.Map; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * @@ -51,8 +49,6 @@ */ public class AppSession { - private static final Logger logger = LoggerFactory.getLogger(AppSession.class); - public final File featureFile; private FeatureWrapper feature; // mutable, can be re-built public final KarateBackend backend; @@ -60,7 +56,7 @@ public class AppSession { public final FeaturePanel featurePanel; public final VarsPanel varsPanel; public final LogPanel logPanel; - + public FeatureWrapper getFeature() { return feature; } @@ -87,7 +83,7 @@ public void runAll() { try { featurePanel.action(AppAction.RUN); } catch (StepException se) { - logger.error("step execution paused."); + backend.getEnv().logger.error("step execution paused."); } } @@ -102,7 +98,7 @@ public AppSession(File featureFile, String envString, boolean test) { headerPanel = new HeaderPanel(this); featurePanel = new FeaturePanel(this); varsPanel = new VarsPanel(this); - logPanel = new LogPanel(null); + logPanel = new LogPanel(backend.getEnv().logger); } else { headerPanel = null; featurePanel = null; From e5faab7aa917cd6a1913a10381ea83eea9c568b9 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Wed, 25 Jul 2018 10:20:08 +0530 Subject: [PATCH 06/23] fix for ui mode file in current working dir --- karate-netty/src/main/java/com/intuit/karate/netty/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karate-netty/src/main/java/com/intuit/karate/netty/Main.java b/karate-netty/src/main/java/com/intuit/karate/netty/Main.java index 68c0c3e52..948407c38 100644 --- a/karate-netty/src/main/java/com/intuit/karate/netty/Main.java +++ b/karate-netty/src/main/java/com/intuit/karate/netty/Main.java @@ -129,7 +129,7 @@ public Object handleExecutionException(ExecutionException ex, ParseResult parseR public Void call() throws Exception { if (tests != null) { if (ui) { - App.main(new String[]{tests.get(0), env}); + App.main(new String[]{new File(tests.get(0)).getAbsolutePath(), env}); } else { if (env != null) { System.setProperty(ScriptBindings.KARATE_ENV, env); From dd0ce783f0c3a9b20fb3b090c83a3f17155a4a49 Mon Sep 17 00:00:00 2001 From: Venu Madhav Chukkapalli Date: Thu, 26 Jul 2018 18:57:53 +0530 Subject: [PATCH 07/23] Issue-425: fix NPE while importing postman collection with item array --- .../intuit/karate/convert/ConvertUtils.java | 104 ++++++++++------ .../intuit/karate/convert/PostmanItem.java | 111 ++++++++++++++++++ .../intuit/karate/convert/PostmanRequest.java | 18 +-- ...eatureBuilder.java => RequestBuilder.java} | 52 +++----- .../main/java/com/intuit/karate/ui/App.java | 6 +- .../karate/convert/ConvertUtilsTest.java | 48 +++++--- ...ilderTest.java => RequestBuilderTest.java} | 18 +-- ...ple-items-and-sub-items.postman_collection | 101 ++++++++++++++++ .../web/wicket/PostmanConvertPanel.java | 6 +- 9 files changed, 346 insertions(+), 118 deletions(-) create mode 100644 karate-core/src/main/java/com/intuit/karate/convert/PostmanItem.java rename karate-core/src/main/java/com/intuit/karate/convert/{FeatureBuilder.java => RequestBuilder.java} (69%) rename karate-core/src/test/java/com/intuit/karate/convert/{FeatureBuilderTest.java => RequestBuilderTest.java} (74%) create mode 100644 karate-core/src/test/java/com/intuit/karate/convert/postman-multiple-items-and-sub-items.postman_collection diff --git a/karate-core/src/main/java/com/intuit/karate/convert/ConvertUtils.java b/karate-core/src/main/java/com/intuit/karate/convert/ConvertUtils.java index 441ad403d..a90888c77 100644 --- a/karate-core/src/main/java/com/intuit/karate/convert/ConvertUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/convert/ConvertUtils.java @@ -28,63 +28,91 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Created by rkumar32 on 7/5/17. */ public class ConvertUtils { - + private static final Logger logger = LoggerFactory.getLogger(ConvertUtils.class); private ConvertUtils() { // only static methods } - public static String toKarateFeature(List requests) { - String scenarios = ""; - for (PostmanRequest request : requests) { - scenarios += request.convert(); + public static String toKarateFeature(List items) { + return toKarateFeature(UUID.randomUUID().toString(), items); + } + + public static String toKarateFeature(String collectionName, List items) { + StringBuilder sb = new StringBuilder("Feature: ").append(collectionName); + sb.append(System.lineSeparator()).append(System.lineSeparator()); + for (PostmanItem item : items) { + sb.append(item.convert()); } - String feature = "Feature: \n\n" + scenarios; - return feature; + return sb.toString(); } - - public static List readPostmanJson(String json) { + + public static List readPostmanJson(String json) { DocumentContext doc = JsonPath.parse(json); List> list = (List) doc.read("$.item"); - List requests = new ArrayList<>(list.size()); + return readPostmanItems(null, list); + } + + private static List readPostmanItems(PostmanItem parent, List> list) { + PostmanItem item; + List requests = new ArrayList<>(list.size()); for (Map map : list) { logger.debug("map: {}", map); - String name = (String) map.get("name"); - Map requestInfo = (Map) map.get("request"); - String url = (String) requestInfo.get("url"); - String method = (String) requestInfo.get("method"); - List> headersList = (List) requestInfo.get("header"); - Map headers = new HashMap<>(); + item = readPosmanItem(parent, map); + requests.add(item); + } + return requests; + } + + private static PostmanItem readPosmanItem(PostmanItem parent, Map itemMap) { + PostmanItem item = new PostmanItem(); + String name = (String) itemMap.get("name"); + item.setName(name); + item.setParent(Optional.ofNullable(parent)); + Map requestInfo = (Map) itemMap.get("request"); + if (requestInfo != null) { + item.setRequest(readPostmanRequest(requestInfo)); + } else { // this may have list of sub-items + List subItems = readPostmanItems(item, (List>) itemMap.get("item")); + item.setItems(Optional.of(subItems)); + } + return item; + } + + private static PostmanRequest readPostmanRequest(Map requestInfo) { + String url = getUrl(requestInfo.get("url")); + String method = (String) requestInfo.get("method"); + List> headersList = (List) requestInfo.get("header"); + Map headers = new HashMap<>(); + if (headersList != null) { for (Map header : headersList) { headers.put((String) header.get("key"), (String) header.get("value")); } - Map bodyInfo = (Map) requestInfo.get("body"); - String body = null; - if (bodyInfo.containsKey("raw")) { - body = ((String) bodyInfo.get("raw")).replace(System.lineSeparator(), ""); - } - else if (bodyInfo.containsKey("formdata")) { - body = ((List) bodyInfo.get("formdata")).toString().replace(System.lineSeparator(), ""); - } - PostmanRequest request = new PostmanRequest(); - request.setName(name); - request.setUrl(url); - request.setMethod(method); - request.setHeaders(headers); - request.setBody(body); - requests.add(request); } - return requests; - } - + Map bodyInfo = (Map) requestInfo.get("body"); + String body = null; + if (bodyInfo.containsKey("raw")) { + body = ((String) bodyInfo.get("raw")).replace(System.lineSeparator(), ""); + } else if (bodyInfo.containsKey("formdata")) { + body = ((List) bodyInfo.get("formdata")).toString().replace(System.lineSeparator(), ""); + } + PostmanRequest request = new PostmanRequest(); + request.setUrl(url); + request.setMethod(method); + request.setHeaders(headers); + request.setBody(body); + return request; + } + + private static String getUrl(Object url) { + return (url instanceof String) ? (String) url : (String) ((Map) url).get("raw"); + } + } diff --git a/karate-core/src/main/java/com/intuit/karate/convert/PostmanItem.java b/karate-core/src/main/java/com/intuit/karate/convert/PostmanItem.java new file mode 100644 index 000000000..44ac3f126 --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/convert/PostmanItem.java @@ -0,0 +1,111 @@ +/* + * The MIT License + * + * Copyright 2017 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.convert; + +import java.util.List; +import java.util.Optional; + +/** + * @author vmchukky + */ + +//http://schema.getpostman.com/json/collection/v2.1.0/docs/index.html +public class PostmanItem { + + private String name; + private PostmanRequest request; // schema says request is mandatory but have seen example collections without it + private Optional parent; // top level item (without a parent becomes a scenario) + private Optional> items; // items within an item become part of the same scenario associated with parent + + public PostmanItem() { + this.items = Optional.empty(); + this.parent = Optional.empty(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = (name == null) ? "" : name; + } + + public PostmanRequest getRequest() { + return request; + } + + public void setRequest(PostmanRequest request) { + this.request = request; + } + + public Optional getParent() { + return parent; + } + + public void setParent(Optional parent) { + this.parent = parent; + } + + public Optional> getItems() { + return items; + } + + public void setItems(Optional> items) { + this.items = items; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[name: ").append(name); + if (items.isPresent()) { + sb.append(items.toString()); + } else { + sb.append(", request: ").append(request.toString()); + } + sb.append("]"); + return sb.toString(); + } + + public String convert() { + StringBuilder sb = new StringBuilder(); + + sb.append(parent.isPresent() ? "# " : "Scenario: "); + sb.append(name).append(System.lineSeparator()); + if (items.isPresent()) { + for (PostmanItem item : items.get()) { + sb.append(item.convert()); + } + } else { + RequestBuilder builder = new RequestBuilder(); + sb.append(builder.addUrl(request.getUrl()) + .addHeaders(request.getHeaders()) + .addBody(request.getBody()) + .addMethod(request.getMethod()) + .build()); + } + return sb.toString(); + } + +} diff --git a/karate-core/src/main/java/com/intuit/karate/convert/PostmanRequest.java b/karate-core/src/main/java/com/intuit/karate/convert/PostmanRequest.java index 9399acfaa..c74e2c043 100644 --- a/karate-core/src/main/java/com/intuit/karate/convert/PostmanRequest.java +++ b/karate-core/src/main/java/com/intuit/karate/convert/PostmanRequest.java @@ -30,8 +30,7 @@ * @author pthomas3 */ public class PostmanRequest { - - private String name; + private String url; private String method; private Map headers; @@ -68,20 +67,11 @@ public String getUrl() { public void setUrl(String url) { this.url = url; } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("[name: ").append(name); - sb.append(", url: ").append(url); + sb.append("[url: ").append(url); sb.append(", method: ").append(method); sb.append(", headers: ").append(headers); sb.append(", body: ").append(body); @@ -89,10 +79,8 @@ public String toString() { return sb.toString(); } - public String convert() { - return new FeatureBuilder() - .addName(name) + return new RequestBuilder() .addUrl(url) .addHeaders(headers) .addBody(body) diff --git a/karate-core/src/main/java/com/intuit/karate/convert/FeatureBuilder.java b/karate-core/src/main/java/com/intuit/karate/convert/RequestBuilder.java similarity index 69% rename from karate-core/src/main/java/com/intuit/karate/convert/FeatureBuilder.java rename to karate-core/src/main/java/com/intuit/karate/convert/RequestBuilder.java index 8dcf4e547..f4b0cfc87 100644 --- a/karate-core/src/main/java/com/intuit/karate/convert/FeatureBuilder.java +++ b/karate-core/src/main/java/com/intuit/karate/convert/RequestBuilder.java @@ -24,78 +24,60 @@ package com.intuit.karate.convert; import com.intuit.karate.StringUtils; + import java.util.Map; /** * Created by rkumar32 on 5/24/17. */ -public class FeatureBuilder { +public class RequestBuilder { - private String name; private String url; private String method; private String headers; private String body; - private final String SCENARIO_TEMPLATE = "Scenario: %s" + // Scenario Description - "Given url " + "%s" + // url - "%s" + // Headers - "%s" + // Body - "When method %s" + System.lineSeparator(); // Method + private final String REQUEST_TEMPLATE = "Given url " + "%s" + // url + "%s" + // Headers + "%s" + // Body + "When method %s" + System.lineSeparator(); // Method - public FeatureBuilder addName(String name) { - if (name != null) { - this.name = name + System.lineSeparator(); - } - else { - this.name = ""; - } - return this; - } - - public FeatureBuilder addUrl(String url) { + public RequestBuilder addUrl(String url) { if (url != null) { this.url = "'" + url + "'" + System.lineSeparator(); - } - else { + } else { throw new IllegalArgumentException("Url is null"); } return this; } - public FeatureBuilder addMethod(String method) { + public RequestBuilder addMethod(String method) { if (url != null) { this.method = method + System.lineSeparator(); - } - else { + } else { throw new IllegalArgumentException("Method is null"); } return this; } - public FeatureBuilder addHeaders(Map headers) { + public RequestBuilder addHeaders(Map headers) { this.headers = ""; - for (Map.Entry entry: headers.entrySet()) { + for (Map.Entry entry : headers.entrySet()) { this.headers += "And header " + entry.getKey() + " = " + "'" + entry.getValue() + "'" + System.lineSeparator(); } return this; } - public FeatureBuilder addBody(String body) { + public RequestBuilder addBody(String body) { if (body != null) { this.body = "And request " + body + System.lineSeparator(); - } - else { + } else { this.body = ""; } return this; } - public String getName() { - return name; - } - public String getUrl() { return url; } @@ -116,12 +98,10 @@ public String build() { if ("POST".equals(method) && StringUtils.isBlank(body)) { throw new IllegalArgumentException("Body can't be null if method is POST"); } - return String.format(SCENARIO_TEMPLATE, name, - url, + return String.format(REQUEST_TEMPLATE, url, headers, body, method); } - -} +} diff --git a/karate-core/src/main/java/com/intuit/karate/ui/App.java b/karate-core/src/main/java/com/intuit/karate/ui/App.java index 7d370d8f5..6ce0e7f48 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/App.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/App.java @@ -26,7 +26,7 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.ScriptBindings; import com.intuit.karate.convert.ConvertUtils; -import com.intuit.karate.convert.PostmanRequest; +import com.intuit.karate.convert.PostmanItem; import javafx.application.Application; import javafx.geometry.Insets; import javafx.scene.Scene; @@ -113,8 +113,8 @@ private void initImportOpenAction(HeaderPanel header, String envString, Stage st header.setImportOpenAction(e -> { File file = chooseFile(stage, "*.postman_collection files", "*.postman_collection"); String json = FileUtils.toString(file); - List requests = ConvertUtils.readPostmanJson(json); - String featureText = ConvertUtils.toKarateFeature(requests); + List items = ConvertUtils.readPostmanJson(json); + String featureText = ConvertUtils.toKarateFeature(file.getName(), items); String featurePath = FileUtils.replaceFileExtension(file.getPath(), "feature"); File featureFile = new File(featurePath); FileUtils.writeToFile(featureFile, featureText); diff --git a/karate-core/src/test/java/com/intuit/karate/convert/ConvertUtilsTest.java b/karate-core/src/test/java/com/intuit/karate/convert/ConvertUtilsTest.java index 9a61b4545..d6c577829 100644 --- a/karate-core/src/test/java/com/intuit/karate/convert/ConvertUtilsTest.java +++ b/karate-core/src/test/java/com/intuit/karate/convert/ConvertUtilsTest.java @@ -1,35 +1,55 @@ package com.intuit.karate.convert; import com.intuit.karate.FileUtils; -import java.io.InputStream; -import java.util.List; import org.junit.Test; -import static org.junit.Assert.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.InputStream; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** - * * @author pthomas3 */ public class ConvertUtilsTest { - - private static final Logger logger = LoggerFactory.getLogger(ConvertUtilsTest.class); - + + private static final Logger logger = LoggerFactory.getLogger(ConvertUtilsTest.class); + @Test - public void testReading() { + public void testReadingSinglePostmanItemWithOneRequest() { InputStream is = getClass().getResourceAsStream("postman-echo-single.postman_collection"); String json = FileUtils.toString(is); - List requests = ConvertUtils.readPostmanJson(json); - logger.debug("list: {}", requests); - assertEquals(1, requests.size()); - PostmanRequest request = requests.get(0); - assertEquals("OAuth1.0 Verify Signature", request.getName()); + List items = ConvertUtils.readPostmanJson(json); + logger.debug("list: {}", items); + assertEquals(1, items.size()); + PostmanItem item = items.get(0); + PostmanRequest request = item.getRequest(); + assertEquals("OAuth1.0 Verify Signature", item.getName()); assertEquals("https://echo.getpostman.com/oauth1", request.getUrl()); assertEquals("GET", request.getMethod()); assertEquals(1, request.getHeaders().size()); assertEquals("OAuth oauth_consumer_key=\"RKCGzna7bv9YD57c\",oauth_signature_method=\"HMAC-SHA1\",oauth_timestamp=\"1442394747\",oauth_nonce=\"UIGipk\",oauth_version=\"1.0\",oauth_signature=\"CaeyGPr2mns1WCq4Cpm5aLvz6Gs=\"", request.getHeaders().get("Authorization")); logger.debug(request.getBody()); } - + + @Test + public void testReadingItemListWithSubItems() { + String collectionFileName = "postman-multiple-items-and-sub-items.postman_collection"; + InputStream is = getClass().getResourceAsStream(collectionFileName); + String json = FileUtils.toString(is); + List items = ConvertUtils.readPostmanJson(json); + logger.debug("list: {}", items); + String featureJson = ConvertUtils.toKarateFeature(collectionFileName, items).trim(); + assertTrue(featureJson.startsWith("Feature: " + collectionFileName)); // assert feature name + assertTrue(featureJson.contains("Scenario: rootItem-1")); // assert scenario names + assertTrue(featureJson.contains("Scenario: rootItem-2")); + assertTrue(featureJson.contains("Scenario: rootItem-3")); + assertTrue(featureJson.contains("# subitem-1-1")); // assert comment for each sub request + assertTrue(featureJson.contains("# subitem-1-2")); + assertTrue(featureJson.contains("# subitem-2-1")); + } + } diff --git a/karate-core/src/test/java/com/intuit/karate/convert/FeatureBuilderTest.java b/karate-core/src/test/java/com/intuit/karate/convert/RequestBuilderTest.java similarity index 74% rename from karate-core/src/test/java/com/intuit/karate/convert/FeatureBuilderTest.java rename to karate-core/src/test/java/com/intuit/karate/convert/RequestBuilderTest.java index d78c66dfa..ace8e5a73 100644 --- a/karate-core/src/test/java/com/intuit/karate/convert/FeatureBuilderTest.java +++ b/karate-core/src/test/java/com/intuit/karate/convert/RequestBuilderTest.java @@ -13,21 +13,21 @@ /** * Created by rkumar32 on 7/5/17. */ -public class FeatureBuilderTest { +public class RequestBuilderTest { - private static final Logger logger = LoggerFactory.getLogger(FeatureBuilderTest.class); + private static final Logger logger = LoggerFactory.getLogger(RequestBuilderTest.class); @Test public void testConverting() { InputStream is = getClass().getResourceAsStream("postman-echo-single.postman_collection"); String json = FileUtils.toString(is); - List requests = ConvertUtils.readPostmanJson(json); - logger.debug("list: {}", requests); - assertEquals(1, requests.size()); - PostmanRequest request = requests.get(0); - FeatureBuilder builder = new FeatureBuilder(); - builder.addName(request.getName()); - assertEquals("OAuth1.0 Verify Signature", builder.getName().trim()); + List items = ConvertUtils.readPostmanJson(json); + logger.debug("list: {}", items); + assertEquals(1, items.size()); + PostmanItem item = items.get(0); + PostmanRequest request = item.getRequest(); + RequestBuilder builder = new RequestBuilder(); + assertEquals("OAuth1.0 Verify Signature", item.getName().trim()); builder.addUrl(request.getUrl()); assertEquals("'https://echo.getpostman.com/oauth1'", builder.getUrl().trim()); builder.addHeaders(request.getHeaders()); diff --git a/karate-core/src/test/java/com/intuit/karate/convert/postman-multiple-items-and-sub-items.postman_collection b/karate-core/src/test/java/com/intuit/karate/convert/postman-multiple-items-and-sub-items.postman_collection new file mode 100644 index 000000000..920f8cb69 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/convert/postman-multiple-items-and-sub-items.postman_collection @@ -0,0 +1,101 @@ +{ + "info":{ + "_postman_id":"test-collection", + "name":"test collection", + "schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item":[ + { + "name":"rootItem-1", + "item":[ + { + "name":"subitem-1-1", + "request":{ + "method":"GET", + "body":{}, + "url":"https://duckduckgo.com/?q=karate+web+service+testing" + }, + "response":[] + }, + { + "name":"subitem-1-2", + "request":{ + "method":"GET", + "header":[ + { + "key":"Content-Type", + "value":"application/json" + } + ], + "body":{}, + "url":"http://schema.getpostman.com/json/collection/v2.1.0/docs/index.html" + }, + "response":[] + } + ] + }, + { + "name":"rootItem-2", + "item":[ + { + "name":"subitem-2-1", + "request":{ + "auth":{ + "type":"basic", + "basic":{ + "username":"testUser", + "password":"testPa$$w0rd", + "showPassword":false + } + }, + "method":"GET", + "header":[ + { + "key":"Content-Type", + "value":"application/x-www-form-urlencoded" + }, + { + "key":"Authorization", + "value":"Basic dGVzdFVzZXI6dGVzdFBhJCR3MHJk" + } + ], + "body":{ + "mode":"urlencoded", + "urlencoded":[ + { + "description":{ + "content":"", + "type":"text/plain" + }, + "key":"si211", + "value":"test", + "type":"text" + }, + { + "description":{ + "content":"", + "type":"text/plain" + }, + "key":"si212", + "value":"06/26/2018", + "type":"text" + } + ] + }, + "url":"https://httpbin.org/basic-auth/testUser/testPa%24%24w0rd" + }, + "response":[] + } + ] + }, + { + "name":"rootItem-3", + "request":{ + "method":"GET", + "body":{}, + "url":"https://github.com/intuit/karate" + }, + "response":[] + } + ] +} \ No newline at end of file diff --git a/karate-web/src/main/java/com/intuit/karate/web/wicket/PostmanConvertPanel.java b/karate-web/src/main/java/com/intuit/karate/web/wicket/PostmanConvertPanel.java index 07cf71232..2f993e20d 100644 --- a/karate-web/src/main/java/com/intuit/karate/web/wicket/PostmanConvertPanel.java +++ b/karate-web/src/main/java/com/intuit/karate/web/wicket/PostmanConvertPanel.java @@ -24,7 +24,7 @@ package com.intuit.karate.web.wicket; import com.intuit.karate.convert.ConvertUtils; -import com.intuit.karate.convert.PostmanRequest; +import com.intuit.karate.convert.PostmanItem; import com.intuit.karate.web.service.KarateService; import com.intuit.karate.web.service.KarateSession; import org.apache.wicket.markup.html.form.Form; @@ -54,8 +54,8 @@ public PostmanConvertPanel(String id) { @Override protected void onSubmit() { logger.debug("text is: {}", text); - List requests = ConvertUtils.readPostmanJson(text); - String feature = ConvertUtils.toKarateFeature(requests); + List items = ConvertUtils.readPostmanJson(text); + String feature = ConvertUtils.toKarateFeature(items); KarateSession session = service.createSession("dev", feature); setResponsePage(new FeaturePage(session.getId())); } From 4944fd7cfb036a07b30f556f8540e9f2e616c712 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Thu, 26 Jul 2018 21:47:45 +0530 Subject: [PATCH 08/23] implemented being able to select tag when calling feature ref #471 this now makes it possible to compose gatling tests out of existing suites more efficiently --- README.md | 11 ++++ .../java/com/intuit/karate/FileUtils.java | 30 ++++++--- .../intuit/karate/cucumber/AsyncFeature.java | 2 +- .../karate/cucumber/CucumberRunner.java | 2 +- .../intuit/karate/cucumber/CucumberUtils.java | 4 +- .../karate/cucumber/FeatureSection.java | 30 ++++++--- .../karate/cucumber/FeatureWrapper.java | 63 +++++++++++++------ .../karate/cucumber/CucumberUtilsTest.java | 2 +- .../karate/cucumber/FeatureProviderTest.java | 2 +- .../intuit/karate/cucumber/called_2.feature | 11 +++- .../intuit/karate/cucumber/caller_2.feature | 2 +- karate-gatling/README.md | 14 ++++- .../intuit/karate/gatling/KarateAction.scala | 4 +- .../karate/gatling/KarateActionBuilder.scala | 5 +- .../src/test/scala/mock/CatsSimulation.scala | 2 +- .../src/test/scala/mock/cats-delete.feature | 6 ++ .../netty/FeatureServerInitializer.java | 2 +- .../karate/web/service/KarateService.java | 2 +- 18 files changed, 141 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 235619d19..ecba126a2 100755 --- a/README.md +++ b/README.md @@ -2823,6 +2823,17 @@ So you get the picture, any kind of complicated 'sign-in' flow can be scripted a Do look at the documentation and example for [`configure headers`](#configure-headers) also as it goes hand-in-hand with `call`. In the above example, the end-result of the `call` to `my-signin.feature` resulted in the `authToken` variable being initialized. Take a look at how the [`configure headers`](#configure-headers) example uses the `authToken` variable. +### Call Tag Selector +You can "select" a single `Scenario` (or `Scenario`-s or `Scenario Outline`-s) by appending a "tag selector" at the end of the feature-file you are calling. For example: + +```cucumber +call read('classpath:my-signin.feature@name=someScenarioName') +``` + +While the tag does not need to be in the `@key=value` form, it is recommended for readability when you start getting into the business of giving meaningful names to your `Scenario`-s. + +This "tag selection" capability is designed for you to be able to "compose" flows out of existing test-suites when using the [Karate Gatling integration](karate-gatling). Normally we recommend that you keep your "re-usable" features lightweight - by limiting them to just one `Scenario`. + ### Data-Driven Features If the argument passed to the [call of a `*.feature` file](#calling-other-feature-files) is a JSON array, something interesting happens. The feature is invoked for each item in the array. Each array element is expected to be a JSON object, and for each object - the behavior will be as described above. diff --git a/karate-core/src/main/java/com/intuit/karate/FileUtils.java b/karate-core/src/main/java/com/intuit/karate/FileUtils.java index d22742fdb..a2aa4f747 100755 --- a/karate-core/src/main/java/com/intuit/karate/FileUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/FileUtils.java @@ -83,6 +83,20 @@ private static String removePrefix(String text) { int pos = text.indexOf(':'); return pos == -1 ? text : text.substring(pos + 1); } + + private static StringUtils.Pair parsePathAndTags(String text) { + int pos = text.indexOf(':'); + text = pos == -1 ? text : text.substring(pos + 1); // remove prefix + pos = text.indexOf('@'); + if (pos == -1) { + text = StringUtils.trimToEmpty(text); + return new StringUtils.Pair(text, null); + } else { + String left = StringUtils.trimToEmpty(text.substring(0, pos)); + String right = StringUtils.trimToEmpty(text.substring(pos)); + return new StringUtils.Pair(left, right); + } + } private static enum PathPrefix { NONE, @@ -93,25 +107,25 @@ private static enum PathPrefix { public static ScriptValue readFile(String text, ScriptContext context) { text = StringUtils.trimToEmpty(text); PathPrefix prefix = isClassPath(text) ? PathPrefix.CLASSPATH : (isFilePath(text) ? PathPrefix.FILE : PathPrefix.NONE); - String fileName = removePrefix(text); - fileName = StringUtils.trimToEmpty(fileName); + StringUtils.Pair pair = parsePathAndTags(text); + text = pair.left; if (isJsonFile(text) || isXmlFile(text) || isJavaScriptFile(text)) { - String contents = readFileAsString(fileName, prefix, context); + String contents = readFileAsString(text, prefix, context); ScriptValue temp = evalKarateExpression(contents, context); return new ScriptValue(temp.getValue(), text); } else if (isTextFile(text) || isGraphQlFile(text)) { - String contents = readFileAsString(fileName, prefix, context); + String contents = readFileAsString(text, prefix, context); return new ScriptValue(contents, text); } else if (isFeatureFile(text)) { - String contents = readFileAsString(fileName, prefix, context); - FeatureWrapper feature = FeatureWrapper.fromString(contents, context.env, text); + String contents = readFileAsString(text, prefix, context); + FeatureWrapper feature = FeatureWrapper.fromString(contents, context.env, text, pair.right); return new ScriptValue(feature, text); } else if (isYamlFile(text)) { - String contents = readFileAsString(fileName, prefix, context); + String contents = readFileAsString(text, prefix, context); DocumentContext doc = JsonUtils.fromYaml(contents); return new ScriptValue(doc, text); } else { - InputStream is = getFileStream(fileName, prefix, context); + InputStream is = getFileStream(text, prefix, context); return new ScriptValue(is, text); } } diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncFeature.java b/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncFeature.java index 2be9cfe6d..07f07b162 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncFeature.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/AsyncFeature.java @@ -42,7 +42,7 @@ public class AsyncFeature implements AsyncAction { public AsyncFeature(FeatureWrapper feature, KarateBackend backend) { this.feature = feature; this.backend = backend; - this.iterator = feature.getSections().iterator(); + this.iterator = feature.getSectionsByCallTag().iterator(); } @Override diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberRunner.java b/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberRunner.java index 8ba069440..96f55409d 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberRunner.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberRunner.java @@ -167,7 +167,7 @@ public static Map runFeature(File file, Map vars } public static Map runFeature(File file, CallContext callContext, KarateReporter reporter) { - FeatureWrapper featureWrapper = FeatureWrapper.fromFile(file, reporter); + FeatureWrapper featureWrapper = FeatureWrapper.fromFile(file, null, reporter); ScriptValueMap scriptValueMap = CucumberUtils.callSync(featureWrapper, callContext); return scriptValueMap.toPrimitiveMap(); } diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberUtils.java b/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberUtils.java index 85d0f0f53..c2cd6f924 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/CucumberUtils.java @@ -148,9 +148,9 @@ public static ScriptValueMap callSync(FeatureWrapper feature, CallContext callCo return result.vars; } - public static void callAsync(String filePath, CallContext callContext) { + public static void callAsync(String filePath, String callTag, CallContext callContext) { File file = FileUtils.getFeatureFile(filePath); - FeatureWrapper feature = FeatureWrapper.fromFile(file); + FeatureWrapper feature = FeatureWrapper.fromFileAndTag(file, callTag); callAsync(feature, callContext); } diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureSection.java b/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureSection.java index a112784d6..5b0d558d5 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureSection.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureSection.java @@ -23,17 +23,21 @@ */ package com.intuit.karate.cucumber; +import gherkin.formatter.model.Tag; +import java.util.List; +import java.util.stream.Collectors; + /** * * @author pthomas3 */ public class FeatureSection { - + private final int index; private final FeatureWrapper feature; private final ScenarioWrapper scenario; - private final ScenarioOutlineWrapper scenarioOutline; - + private final ScenarioOutlineWrapper scenarioOutline; + public FeatureSection(int index, FeatureWrapper feature, ScenarioWrapper scenario, ScenarioOutlineWrapper scenarioOutline) { this.index = index; this.feature = feature; @@ -46,13 +50,23 @@ public FeatureSection(int index, FeatureWrapper feature, ScenarioWrapper scenari } } + public List getTags() { + List tags; + if (isOutline()) { + tags = scenarioOutline.getScenarioOutline().getGherkinModel().getTags(); + } else { + tags = scenario.getScenario().getGherkinModel().getTags(); + } + return tags.stream().map(t -> t.getName()).collect(Collectors.toList()); + } + public int getIndex() { return index; - } + } public FeatureWrapper getFeature() { return feature; - } + } public ScenarioWrapper getScenario() { return scenario; @@ -61,11 +75,11 @@ public ScenarioWrapper getScenario() { public ScenarioOutlineWrapper getScenarioOutline() { return scenarioOutline; } - + public boolean isOutline() { return scenarioOutline != null; } - + public int getLine() { if (isOutline()) { return scenarioOutline.getLine(); @@ -73,5 +87,5 @@ public int getLine() { return scenario.getLine(); } } - + } diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java b/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java index af2de3a28..8ac8f23da 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java @@ -41,6 +41,7 @@ public class FeatureWrapper { private final String path; + private final String callTag; private final String text; private final List lines; private final CucumberFeature feature; @@ -52,36 +53,40 @@ public ScriptEnv getEnv() { return scriptEnv; } + public String getCallTag() { + return callTag; + } + public void setEnv(ScriptEnv scriptEnv) { this.scriptEnv = scriptEnv; } - - public static FeatureWrapper fromFile(File file) { - return fromFile(file, Thread.currentThread().getContextClassLoader(), null); - } - - public static FeatureWrapper fromFile(File file, KarateReporter reporter) { - return fromFile(file, Thread.currentThread().getContextClassLoader(), reporter); + + public static FeatureWrapper fromFileAndTag(File file, String callTag) { + return fromFile(file, callTag, Thread.currentThread().getContextClassLoader(), null); } - public static FeatureWrapper fromFile(File file, ClassLoader classLoader, KarateReporter reporter) { + public static FeatureWrapper fromFile(File file, String callTag, KarateReporter reporter) { + return fromFile(file, callTag, Thread.currentThread().getContextClassLoader(), reporter); + } + + public static FeatureWrapper fromFile(File file, String callTag, ClassLoader classLoader, KarateReporter reporter) { String text = FileUtils.toString(file); ScriptEnv env = new ScriptEnv(null, file.getParentFile(), file.getName(), classLoader, reporter); - return new FeatureWrapper(text, env, file.getPath()); + return new FeatureWrapper(text, env, file.getPath(), callTag); } public static FeatureWrapper fromFile(File file, ScriptEnv env) { String text = FileUtils.toString(file); - return new FeatureWrapper(text, env, file.getPath()); + return new FeatureWrapper(text, env, file.getPath(), null); } - public static FeatureWrapper fromString(String text, ScriptEnv scriptEnv, String path) { - return new FeatureWrapper(text, scriptEnv, path); + public static FeatureWrapper fromString(String text, ScriptEnv scriptEnv, String path, String callTag) { + return new FeatureWrapper(text, scriptEnv, path, callTag); } public static FeatureWrapper fromStream(InputStream is, ScriptEnv scriptEnv, String path) { String text = FileUtils.toString(is); - return new FeatureWrapper(text, scriptEnv, path); + return new FeatureWrapper(text, scriptEnv, path, null); } public String joinLines(int startLine, int endLine) { @@ -104,7 +109,7 @@ public String joinLines() { public List getLines() { return lines; } - + public String getFirstScenarioName() { if (featureSections == null || featureSections.isEmpty()) { return null; @@ -115,7 +120,7 @@ public String getFirstScenarioName() { } else { return fs.getScenario().getScenario().getGherkinModel().getName(); } - } + } public CucumberFeature getFeature() { return feature; @@ -125,6 +130,25 @@ public List getSections() { return featureSections; } + public List getSectionsByCallTag() { + if (callTag == null) { + return featureSections; + } + List filtered = new ArrayList(featureSections.size()); + for (FeatureSection fs : getSections()) { + List tags = fs.getTags(); + if (tags == null || tags.isEmpty()) { + continue; + } + for (String tag : tags) { + if (callTag.equals(tag)) { + filtered.add(fs); + } + } + } + return filtered; + } + public String getPath() { return path; } @@ -135,7 +159,7 @@ public String getText() { public FeatureWrapper addLine(int index, String line) { lines.add(index, line); - return new FeatureWrapper(joinLines(), scriptEnv, path); + return new FeatureWrapper(joinLines(), scriptEnv, path, callTag); } public FeatureSection getSection(int sectionIndex) { @@ -166,16 +190,17 @@ public FeatureWrapper replaceLines(int start, int end, String text) { lines.remove(start); } lines.set(start, text); - return new FeatureWrapper(joinLines(), scriptEnv, path); + return new FeatureWrapper(joinLines(), scriptEnv, path, callTag); } public FeatureWrapper removeLine(int index) { lines.remove(index); - return new FeatureWrapper(joinLines(), scriptEnv, path); + return new FeatureWrapper(joinLines(), scriptEnv, path, callTag); } - private FeatureWrapper(String text, ScriptEnv scriptEnv, String path) { + private FeatureWrapper(String text, ScriptEnv scriptEnv, String path, String callTag) { this.path = path; + this.callTag = callTag; this.text = text; this.scriptEnv = scriptEnv; this.feature = CucumberUtils.parse(text, path); diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java b/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java index 7497f7874..914ac2658 100644 --- a/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java @@ -141,7 +141,7 @@ public void testMultiLineEdit() { public void testIdentifyingStepWhichIsAnHttpCall() { String text = "Feature:\nScenario:\n* method post"; ScriptEnv env = getEnv(); - FeatureWrapper fw = FeatureWrapper.fromString(text, env, "dummy.feature"); + FeatureWrapper fw = FeatureWrapper.fromString(text, env, "dummy.feature", null); printLines(fw.getLines()); StepWrapper step = fw.getSections().get(0).getScenario().getSteps().get(0); logger.debug("step name: '{}'", step.getStep().getName()); diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureProviderTest.java b/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureProviderTest.java index c6fc2e08d..8b147522a 100644 --- a/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureProviderTest.java +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/FeatureProviderTest.java @@ -50,7 +50,7 @@ private ScriptValueMap getRequest(String name) { @Test public void testServer() { File file = FileUtils.getFileRelativeTo(getClass(), "server.feature"); - FeatureWrapper featureWrapper = FeatureWrapper.fromFile(file); + FeatureWrapper featureWrapper = FeatureWrapper.fromFileAndTag(file, null); FeatureProvider provider = new FeatureProvider(featureWrapper); ScriptValueMap vars = provider.handle(getRequest("Billie")); Match.equals(vars.get("response").getAsMap(), "{ id: 1, name: 'Billie' }"); diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/called_2.feature b/karate-core/src/test/java/com/intuit/karate/cucumber/called_2.feature index 71983496f..6385b52f1 100644 --- a/karate-core/src/test/java/com/intuit/karate/cucumber/called_2.feature +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/called_2.feature @@ -1,6 +1,13 @@ @ignore Feature: +@foo=bar1 Scenario: -* def step1 = 'step1' -* def step2 = 'step2' +* print 'bar1-1' +* print 'bar1-2' + +@foo=bar2 +Scenario: +* print 'bar2-1' +* print 'bar2-2' + diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/caller_2.feature b/karate-core/src/test/java/com/intuit/karate/cucumber/caller_2.feature index 6c0043282..8aa2fb1af 100644 --- a/karate-core/src/test/java/com/intuit/karate/cucumber/caller_2.feature +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/caller_2.feature @@ -4,5 +4,5 @@ Feature: Scenario: * def before = 'before' * print 1 + 2 -* def callresult = call read('called_2.feature') +* def callresult = call read('called_2.feature@foo=bar2') * def after = 'after' diff --git a/karate-gatling/README.md b/karate-gatling/README.md index 24ceca515..c23ce78b9 100644 --- a/karate-gatling/README.md +++ b/karate-gatling/README.md @@ -53,7 +53,7 @@ class CatsSimulation extends Simulation { ) val create = scenario("create").exec(karateFeature("classpath:mock/cats-create.feature")) - val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature")) + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature@name=delete")) setUp( create.inject(rampUsers(10) over (5 seconds)).protocols(protocol), @@ -62,6 +62,14 @@ class CatsSimulation extends Simulation { } ``` +### `karateProtocol` +This piece is needed because Karate is responsible for making HTTP requests while Gatling is only measuring the timings and managing threads. In order for HTTP requests to "aggregate" correctly in the Gatling report, you need to declare the URL patterns involved in your test. For example, in the example above, the `{id}` would be random - and Gatling would by default report each one as a different request. You also need to group requests by the HTTP method (`get`, `post` etc.) and you can also set a pause time (in milliseconds) if needed. We recommend you set that to `0` for everything unless you really need to artifically limit the requests per second. Make sure you wire up the `protocol` in the Gatling `setUp`. + +### `karateFeature` +This executes a whole Karate feature as a "flow". Note how you can have concurrent flows in the same Gatling simulation. + +#### Tag Selector +In the code above, note how a single `Scenario` (or multiple) can be "chosen" by appending the tag name to the `Feature` path. This allows you to re-use only selected tests out of your existing functional or regression test suites for composing a performance test-suite. + +> The tag does not need to be in the `@key=value` form and you can use the plain "`@foo`" form if you want to. But using the pattern `@name=someName` is arguably more readable when it comes to giving your various `Scenario`-s meaningful names. -* `karateProtocol` - this piece is needed because Karate is responsible for making HTTP requests while Gatling is only measuring the timings and managing threads. In order for HTTP requests to "aggregate" correctly in the Gatling report, you need to declare the URL patterns involved in your test. For example, in the example above, the `{id}` would be random - and Gatling would by default report each one as a different request. You also need to group requests by the HTTP method (`get`, `post` etc.) and you can also set a pause time (in milliseconds) if needed. We recommend you set that to `0` for everything unless you really need to artifically limit the requests per second. Make sure you wire up the `protocol` in the Gatling `setUp`. -* `karateFeature` - this executes a whole Karate feature as a "flow". Note how you can have concurrent flows in the same Gatling simulation. diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala index b91c95190..af07c6bbe 100644 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala @@ -23,7 +23,7 @@ class KarateActor extends Actor { } } -class KarateAction(val name: String, val protocol: KarateProtocol, val system: ActorSystem, val statsEngine: StatsEngine, val next: Action) extends ExitableAction { +class KarateAction(val name: String, val callTag: String, val protocol: KarateProtocol, val system: ActorSystem, val statsEngine: StatsEngine, val next: Action) extends ExitableAction { def getActor(): ActorRef = { val actorName = new File(name).getName + "-" + protocol.actorCount.incrementAndGet() @@ -100,7 +100,7 @@ class KarateAction(val name: String, val protocol: KarateProtocol, val system: A val asyncNext: Runnable = () => next ! session val callContext = new CallContext(null, 0, null, -1, false, true, null, asyncSystem, asyncNext, stepInterceptor) - CucumberUtils.callAsync(name, callContext) + CucumberUtils.callAsync(name, callTag, callContext) } diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala index 94949f86d..5d0d970f6 100644 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala @@ -5,8 +5,11 @@ import io.gatling.core.action.builder.ActionBuilder import io.gatling.core.structure.ScenarioContext class KarateActionBuilder(requestName: String) extends ActionBuilder { + val pos = requestName.indexOf('@') + val name = if (pos == -1) requestName else requestName.substring(0, pos) + val callTag = if (pos == -1) null else requestName.substring(pos) override def build(ctx: ScenarioContext, next: Action): Action = { val karateComponents = ctx.protocolComponentsRegistry.components(KarateProtocol.KarateProtocolKey) - new KarateAction(requestName, karateComponents.protocol, karateComponents.system, ctx.coreComponents.statsEngine, next) + new KarateAction(name, callTag, karateComponents.protocol, karateComponents.system, ctx.coreComponents.statsEngine, next) } } diff --git a/karate-gatling/src/test/scala/mock/CatsSimulation.scala b/karate-gatling/src/test/scala/mock/CatsSimulation.scala index 74b2971a9..7228fbe28 100644 --- a/karate-gatling/src/test/scala/mock/CatsSimulation.scala +++ b/karate-gatling/src/test/scala/mock/CatsSimulation.scala @@ -14,7 +14,7 @@ class CatsSimulation extends Simulation { ) val create = scenario("create").exec(karateFeature("classpath:mock/cats-create.feature")) - val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature")) + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature@name=delete")) setUp( create.inject(rampUsers(10) over (5 seconds)).protocols(protocol), diff --git a/karate-gatling/src/test/scala/mock/cats-delete.feature b/karate-gatling/src/test/scala/mock/cats-delete.feature index cb7e18f04..6e6f3a703 100644 --- a/karate-gatling/src/test/scala/mock/cats-delete.feature +++ b/karate-gatling/src/test/scala/mock/cats-delete.feature @@ -3,6 +3,12 @@ Feature: delete all cats found Background: * url karate.properties['mock.cats.url'] + Scenario: this scenario will be ignored because the gatling script looks for the tag @name=delete + * print 'this should not appear in the logs !' + When method get + Then status 400 + + @name=delete Scenario: get all cats and then delete each by id When method get Then status 200 diff --git a/karate-netty/src/main/java/com/intuit/karate/netty/FeatureServerInitializer.java b/karate-netty/src/main/java/com/intuit/karate/netty/FeatureServerInitializer.java index 0489a6a84..9e3249b7c 100644 --- a/karate-netty/src/main/java/com/intuit/karate/netty/FeatureServerInitializer.java +++ b/karate-netty/src/main/java/com/intuit/karate/netty/FeatureServerInitializer.java @@ -46,7 +46,7 @@ public class FeatureServerInitializer extends ChannelInitializer public FeatureServerInitializer(SslContext sslCtx, File featureFile, Map vars, Runnable stopFunction) { this.sslCtx = sslCtx; - FeatureWrapper featureWrapper = FeatureWrapper.fromFile(featureFile); + FeatureWrapper featureWrapper = FeatureWrapper.fromFileAndTag(featureFile, null); provider = new FeatureProvider(featureWrapper, vars); this.stopFunction = stopFunction; } diff --git a/karate-web/src/main/java/com/intuit/karate/web/service/KarateService.java b/karate-web/src/main/java/com/intuit/karate/web/service/KarateService.java index b3b639867..065386c44 100644 --- a/karate-web/src/main/java/com/intuit/karate/web/service/KarateService.java +++ b/karate-web/src/main/java/com/intuit/karate/web/service/KarateService.java @@ -64,7 +64,7 @@ public KarateSession createSession(String envString, String featureText) { String sessionId = uuid.toString(); WebSocketLogAppender appender = new WebSocketLogAppender(sessionId); ScriptEnv env = ScriptEnv.init(envString, new File("."), new String[]{"src/test/java"}); - FeatureWrapper feature = FeatureWrapper.fromString(featureText, env, null); + FeatureWrapper feature = FeatureWrapper.fromString(featureText, env, null, null); return initSessionBackend(feature, appender); } From 43d0cc669e9b3af7011b147986f26306989a001d Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Fri, 27 Jul 2018 11:24:26 +0530 Subject: [PATCH 09/23] adding example of using Nil for gatling pause-times, doc edits --- karate-gatling/README.md | 24 ++++++++++++------- .../src/test/scala/mock/CatsSimulation.scala | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/karate-gatling/README.md b/karate-gatling/README.md index c23ce78b9..e6cc45ca7 100644 --- a/karate-gatling/README.md +++ b/karate-gatling/README.md @@ -32,7 +32,7 @@ Refer: https://github.com/ptrthomas/karate-gatling-demo This demo project happens to depend on [`karate-netty`](../karate-netty) which already depends on `karate-apache`. It is worth calling out that we are perf-testing [Karate test-doubles](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) here ! A truly self-contained demo. ## Limitations -As of now the Gatling concept of ["throttle" and related syntax](https://gatling.io/docs/2.3/general/simulation_setup/#simulation-setup-throttling) is not supported. Most teams don't need this, but you can declare "pause" times in Karate, see below. +As of now the Gatling concept of ["throttle" and related syntax](https://gatling.io/docs/2.3/general/simulation_setup/#simulation-setup-throttling) is not supported. Most teams don't need this, but you can declare "pause" times in Karate, see [`pauseFor()`](#pauseFor). ## Usage @@ -48,7 +48,7 @@ import scala.concurrent.duration._ class CatsSimulation extends Simulation { val protocol = karateProtocol( - "/cats/{id}" -> pauseFor("get" -> 10, "delete" -> 20), + "/cats/{id}" -> Nil, "/cats" -> pauseFor("get" -> 15, "post" -> 25) ) @@ -62,14 +62,22 @@ class CatsSimulation extends Simulation { } ``` -### `karateProtocol` -This piece is needed because Karate is responsible for making HTTP requests while Gatling is only measuring the timings and managing threads. In order for HTTP requests to "aggregate" correctly in the Gatling report, you need to declare the URL patterns involved in your test. For example, in the example above, the `{id}` would be random - and Gatling would by default report each one as a different request. You also need to group requests by the HTTP method (`get`, `post` etc.) and you can also set a pause time (in milliseconds) if needed. We recommend you set that to `0` for everything unless you really need to artifically limit the requests per second. Make sure you wire up the `protocol` in the Gatling `setUp`. +### `karateProtocol()` +This piece is needed because Karate is responsible for making HTTP requests while Gatling is only measuring the timings and managing threads. In order for HTTP requests to "aggregate" correctly in the Gatling report, you need to declare the URL patterns involved in your test. For example, in the example above, the `{id}` would be random - and Gatling would by default report each one as a different request. -### `karateFeature` -This executes a whole Karate feature as a "flow". Note how you can have concurrent flows in the same Gatling simulation. +#### `pauseFor()` + +You can also set pause times (in milliseconds) per URL pattern *and* HTTP method (`get`, `post` etc.) if needed (see [limitations](#limitations)). + +We recommend you set that to `0` for everything unless you really need to artifically limit the requests per second. Note how you can use `Nil` to default to `0` for all HTTP methods for a URL pattern. Make sure you wire up the `protocol` in the Gatling `setUp`. + +### `karateFeature()` +This declares a whole Karate feature as a "flow". Note how you can have concurrent flows in the same Gatling simulation. #### Tag Selector -In the code above, note how a single `Scenario` (or multiple) can be "chosen" by appending the tag name to the `Feature` path. This allows you to re-use only selected tests out of your existing functional or regression test suites for composing a performance test-suite. +In the code above, note how a single `Scenario` (or multiple) can be "chosen" by appending the [tag](https://github.com/intuit/karate#cucumber-tags) name to the `Feature` path. This allows you to re-use only selected tests out of your existing functional or regression test suites for composing a performance test-suite. + +If multiple `Scenario`-s have the tag on them, they will all be executed. The order of execution will be the order in which they appear in the `Feature`. -> The tag does not need to be in the `@key=value` form and you can use the plain "`@foo`" form if you want to. But using the pattern `@name=someName` is arguably more readable when it comes to giving your various `Scenario`-s meaningful names. +> The tag does not need to be in the `@key=value` form and you can use the plain "`@foo`" form if you want to. But using the pattern `@name=someName` is arguably more readable when it comes to giving multiple `Scenario`-s meaningful names. diff --git a/karate-gatling/src/test/scala/mock/CatsSimulation.scala b/karate-gatling/src/test/scala/mock/CatsSimulation.scala index 7228fbe28..fbdcbe13b 100644 --- a/karate-gatling/src/test/scala/mock/CatsSimulation.scala +++ b/karate-gatling/src/test/scala/mock/CatsSimulation.scala @@ -9,7 +9,7 @@ class CatsSimulation extends Simulation { MockUtils.startServer() val protocol = karateProtocol( - "/cats/{id}" -> pauseFor("get" -> 10, "delete" -> 20), + "/cats/{id}" -> Nil, "/cats" -> pauseFor("get" -> 15, "post" -> 25) ) From 0bb003a761a44e804250b5792704c4c5e830ee62 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Fri, 27 Jul 2018 19:19:43 +0530 Subject: [PATCH 10/23] added some fun examples for finding index of match in arrays --- karate-gatling/README.md | 2 +- .../intuit/karate/junit4/demos/js-arrays.feature | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/karate-gatling/README.md b/karate-gatling/README.md index e6cc45ca7..bc75f37c9 100644 --- a/karate-gatling/README.md +++ b/karate-gatling/README.md @@ -32,7 +32,7 @@ Refer: https://github.com/ptrthomas/karate-gatling-demo This demo project happens to depend on [`karate-netty`](../karate-netty) which already depends on `karate-apache`. It is worth calling out that we are perf-testing [Karate test-doubles](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) here ! A truly self-contained demo. ## Limitations -As of now the Gatling concept of ["throttle" and related syntax](https://gatling.io/docs/2.3/general/simulation_setup/#simulation-setup-throttling) is not supported. Most teams don't need this, but you can declare "pause" times in Karate, see [`pauseFor()`](#pauseFor). +As of now the Gatling concept of ["throttle" and related syntax](https://gatling.io/docs/2.3/general/simulation_setup/#simulation-setup-throttling) is not supported. Most teams don't need this, but you can declare "pause" times in Karate, see [`pauseFor()`](#pausefor). ## Usage diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature index b8aa59dac..b05d61321 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature @@ -77,6 +77,22 @@ Scenario: karate filter operation, using array indexes * def res = karate.filter(list, fun) * match res == [1, 3] +Scenario: karate find index of first match (primitive) + * def list = [1, 2, 3, 4] + * def searchFor = 3 + * def foundAt = [] + * def fun = function(x, i){ if (x == searchFor) foundAt.add(i) } + * eval karate.forEach(list, fun) + * match foundAt == [2] + +Scenario: karate find index of first match (complex) + * def list = [{ a: 1, b: 'x'}, { a: 2, b: 'y'}, { a: 3, b: 'z'}] + * def searchFor = { a: 2, b: '#string'} + * def foundAt = [] + * def fun = function(x, i){ if (karate.match(x, searchFor).pass) foundAt.add(i) } + * eval karate.forEach(list, fun) + * match foundAt == [1] + Scenario: get last array element (js) * def list = [1, 2, 3, 4] * def last = list[list.length-1] From 6e1609469a1ba5d7e54e56481cff565bc8f0513a Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Sun, 29 Jul 2018 17:05:03 +0530 Subject: [PATCH 11/23] minor doc edits --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ecba126a2..16880f408 100755 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ And you don't need to create additional Java classes for any of the payloads tha * Tests are super-readable - as scenario data can be expressed in-line, in human-friendly [JSON](#json), [XML](#xml), Cucumber [Scenario](#the-cucumber-way) Outline [tables](#table), or a [payload builder](#set-multiple) approach [unique to Karate](https://gist.github.com/ptrthomas/d6beb17e92a43220d254af942e3ed3d9) * Express expected results as readable, well-formed JSON or XML, and [assert in a single step](#match) that the entire response payload (no matter how complex or deeply nested) - is as expected * Comprehensive [assertion capabilities](https://github.com/intuit/karate#fuzzy-matching) - and failures clearly report which data element (and path) is not as expected, for easy troubleshooting of even large payloads -* [Embedded UI](https://github.com/intuit/karate/wiki/Karate-UI) for stepping through a script in debug mode where you can even re-play a step while editing it - a huge time-saver +* [Embedded UI](https://github.com/intuit/karate/wiki/Karate-UI) for stepping through a script in debug mode where you can even [re-play a step while editing it](https://twitter.com/ptrthomas/status/889356965461217281) - a huge time-saver * Simpler and more [powerful alternative](https://twitter.com/KarateDSL/status/878984854012022784) to JSON-schema for [validating payload structure](#schema-validation) and format that even supports cross-field / domain validation logic * Scripts can [call other scripts](#calling-other-feature-files) - which means that you can easily re-use and maintain authentication and 'set up' flows efficiently, across multiple tests * Embedded JavaScript engine that allows you to build a library of [re-usable functions](#calling-javascript-functions) that suit your specific environment or organization From f92c2732a3928a1d2e64068a27bf7e910aef1ac5 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Mon, 30 Jul 2018 15:00:15 +0530 Subject: [PATCH 12/23] defensive coding to ensure well-formed report json / xml output ref #475 --- .../main/java/com/intuit/karate/FileUtils.java | 17 +++++++++++++++++ .../cucumber/KarateJunitAndJsonReporter.java | 8 +++++++- .../karate/cucumber/KarateJunitFormatter.java | 7 ++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/karate-core/src/main/java/com/intuit/karate/FileUtils.java b/karate-core/src/main/java/com/intuit/karate/FileUtils.java index a2aa4f747..c14c04d20 100755 --- a/karate-core/src/main/java/com/intuit/karate/FileUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/FileUtils.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import static com.intuit.karate.Script.evalKarateExpression; +import java.nio.channels.FileChannel; import java.util.Properties; import org.slf4j.LoggerFactory; @@ -388,5 +389,21 @@ public static String getKarateVersion() { return UNKNOWN; } } + + public static void renameFileIfZeroBytes(String fileName) { + File file = new File(fileName); + if (!file.exists()) { + logger.warn("file not found, previous write operation may have failed: {}", fileName); + } else if (file.length() == 0) { + logger.warn("file size is zero bytes, previous write operation may have failed: {}", fileName); + try { + File dest = new File(fileName + ".fail"); + file.renameTo(dest); + logger.warn("renamed zero length file to: {}", dest.getName()); + } catch (Exception e) { + logger.warn("failed to rename zero length file: {}", e.getMessage()); + } + } + } } diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitAndJsonReporter.java b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitAndJsonReporter.java index 2fa3f9ca2..dd20db4b2 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitAndJsonReporter.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitAndJsonReporter.java @@ -24,6 +24,7 @@ package com.intuit.karate.cucumber; import com.intuit.karate.CallContext; +import com.intuit.karate.FileUtils; import com.intuit.karate.StringUtils; import cucumber.runtime.formatter.CucumberJSONFormatter; import gherkin.formatter.model.Background; @@ -46,6 +47,8 @@ public class KarateJunitAndJsonReporter extends KarateReporterBase { private final KarateJunitFormatter junit; private final CucumberJSONFormatter json; + private final String xmlReportPath; + private final String jsonReportPath; private Exception failureReason; @@ -62,10 +65,11 @@ public KarateJunitFormatter getJunitFormatter() { } public KarateJunitAndJsonReporter(String featurePath, String reportPath) throws IOException { + this.xmlReportPath = reportPath; junit = new KarateJunitFormatter(featurePath, reportPath); int pos = reportPath.lastIndexOf('.'); // foo/bar.xml String basePath = reportPath.substring(0, pos); - String jsonReportPath = basePath + ".json"; + jsonReportPath = basePath + ".json"; tempFilePath = basePath + ".log"; FileWriter fileWriter = new FileWriter(jsonReportPath); json = new CucumberJSONFormatter(fileWriter); @@ -160,6 +164,8 @@ public void done() { public void close() { junit.close(); json.close(); + FileUtils.renameFileIfZeroBytes(xmlReportPath); + FileUtils.renameFileIfZeroBytes(jsonReportPath); } @Override diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java index 5f3054bad..7bb1b77b7 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateJunitFormatter.java @@ -25,6 +25,7 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.StringUtils; +import com.intuit.karate.exception.KarateException; import cucumber.runtime.CucumberException; import cucumber.runtime.io.URLOutputStream; import cucumber.runtime.io.UTF8OutputStreamWriter; @@ -47,7 +48,6 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; @@ -189,8 +189,9 @@ public void done() { StreamResult result = new StreamResult(out); DOMSource source = new DOMSource(doc); trans.transform(source, result); - } catch (TransformerException e) { - throw new CucumberException("Error while transforming.", e); + out.close(); // this should flush() as well + } catch (Exception e) { + throw new KarateException("error saving junit xml file", e); } logger.trace("<< {}", reportPath); } From 3e3cac395ab45c08786cd5c101bba375084b4252 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Mon, 30 Jul 2018 18:16:20 +0530 Subject: [PATCH 13/23] adding unit test for #475 --- .../src/main/java/com/intuit/karate/FileUtils.java | 1 - .../src/test/java/com/intuit/karate/FileUtilsTest.java | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/karate-core/src/main/java/com/intuit/karate/FileUtils.java b/karate-core/src/main/java/com/intuit/karate/FileUtils.java index c14c04d20..6bf66984d 100755 --- a/karate-core/src/main/java/com/intuit/karate/FileUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/FileUtils.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import static com.intuit.karate.Script.evalKarateExpression; -import java.nio.channels.FileChannel; import java.util.Properties; import org.slf4j.LoggerFactory; diff --git a/karate-core/src/test/java/com/intuit/karate/FileUtilsTest.java b/karate-core/src/test/java/com/intuit/karate/FileUtilsTest.java index 89134adce..004900e26 100755 --- a/karate-core/src/test/java/com/intuit/karate/FileUtilsTest.java +++ b/karate-core/src/test/java/com/intuit/karate/FileUtilsTest.java @@ -54,4 +54,14 @@ public void testWindowsFileNames() { assertEquals("com.intuit.karate.cucumber.scenario", fixed); } + @Test + public void testRenameZeroLengthFile() { + long time = System.currentTimeMillis(); + String name = "target/" + time + ".json"; + FileUtils.writeToFile(new File(name), ""); + FileUtils.renameFileIfZeroBytes(name); + File file = new File(name + ".fail"); + assertTrue(file.exists()); + } + } From d5b5385d87417a5201153bf73390359296c23937 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Wed, 1 Aug 2018 09:17:21 +0530 Subject: [PATCH 14/23] configure report had no effect #478 --- .../java/com/intuit/karate/cucumber/KarateReporterBase.java | 2 +- karate-demo/src/test/java/demo/polling/get.feature | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateReporterBase.java b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateReporterBase.java index 0c7ff48c4..7643c526d 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/KarateReporterBase.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/KarateReporterBase.java @@ -86,7 +86,7 @@ public void exampleBegin(ScenarioWrapper scenario, CallContext callContext) { @Override // see the step() method for an explanation of this hack public void karateStep(Step step, Match match, Result result, CallContext callContext, ScriptContext context) { - boolean isPrint = true; // TODO step.getName().startsWith("print "); + boolean isPrint = step.getName().startsWith("print "); boolean isNoise = false; // TODO step.getKeyword().charAt(0) == '*'; boolean showAllSteps = true; // TODO context == null ? true : context.getConfig().isShowAllSteps(); boolean logEnabled = context == null ? true : context.getConfig().isLogEnabled(); diff --git a/karate-demo/src/test/java/demo/polling/get.feature b/karate-demo/src/test/java/demo/polling/get.feature index ac5ec819a..9e95e4233 100644 --- a/karate-demo/src/test/java/demo/polling/get.feature +++ b/karate-demo/src/test/java/demo/polling/get.feature @@ -2,7 +2,7 @@ Feature: Scenario: - +* configure report = false Given url demoBaseUrl And path 'greeting' When method get From 19cedf138dc917ffe545b28819158d8f6d4c19ca Mon Sep 17 00:00:00 2001 From: Venu Madhav Chukkapalli Date: Wed, 1 Aug 2018 16:38:01 +0530 Subject: [PATCH 15/23] Issue-479: karate-ui enhanced to create-new/edit-existing/save-modified features --- .../karate/cucumber/FeatureWrapper.java | 8 +- .../main/java/com/intuit/karate/ui/App.java | 81 ++++++++++++++++--- .../java/com/intuit/karate/ui/AppSession.java | 5 ++ .../com/intuit/karate/ui/FeaturePanel.java | 6 ++ .../com/intuit/karate/ui/HeaderPanel.java | 28 ++++--- .../karate/cucumber/CucumberUtilsTest.java | 6 +- 6 files changed, 107 insertions(+), 27 deletions(-) diff --git a/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java b/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java index 8ac8f23da..84587c527 100644 --- a/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java +++ b/karate-core/src/main/java/com/intuit/karate/cucumber/FeatureWrapper.java @@ -91,8 +91,8 @@ public static FeatureWrapper fromStream(InputStream is, ScriptEnv scriptEnv, Str public String joinLines(int startLine, int endLine) { StringBuilder sb = new StringBuilder(); - if (endLine > lines.size() - 1) { - endLine = lines.size() - 1; + if (endLine > lines.size()) { + endLine = lines.size(); } for (int i = startLine; i < endLine; i++) { String line = lines.get(i); @@ -198,6 +198,10 @@ public FeatureWrapper removeLine(int index) { return new FeatureWrapper(joinLines(), scriptEnv, path, callTag); } + public FeatureWrapper replaceText(String newText) { + return new FeatureWrapper(newText, scriptEnv, path, callTag); + } + private FeatureWrapper(String text, ScriptEnv scriptEnv, String path, String callTag) { this.path = path; this.callTag = callTag; diff --git a/karate-core/src/main/java/com/intuit/karate/ui/App.java b/karate-core/src/main/java/com/intuit/karate/ui/App.java index 6ce0e7f48..81f90047f 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/App.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/App.java @@ -27,6 +27,7 @@ import com.intuit.karate.ScriptBindings; import com.intuit.karate.convert.ConvertUtils; import com.intuit.karate.convert.PostmanItem; +import com.intuit.karate.cucumber.FeatureWrapper; import javafx.application.Application; import javafx.geometry.Insets; import javafx.scene.Scene; @@ -50,9 +51,20 @@ public class App extends Application { private final FileChooser fileChooser = new FileChooser(); + private boolean needsNameToSave = false; private File workingDir = new File("."); private final BorderPane rootPane = new BorderPane(); + private static final String DEFAULT_FEATURE_NAME = "noname.feature"; + private static final String DEFAULT_FEATURE_TEXT = "Feature: brief description of what is being tested\n\n" + + "Scenario: description of this scenario\n" + + "# steps for this scenario\n" + + "Given url 'https://duckduckgo.com'\n" + + "And param q = 'intuit karate'\n" + + "When method GET\nThen status 200\n\n" + + "Scenario: a different scenario\n" + + "# steps for this other scenario"; + public static Font getDefaultFont() { return Font.font("Courier"); } @@ -61,17 +73,28 @@ private File chooseFile(Stage stage, String description, String extension) { fileChooser.setTitle("Choose Feature File"); fileChooser.setInitialDirectory(workingDir); FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(description, extension); - fileChooser.getExtensionFilters().add(extFilter); + fileChooser.getExtensionFilters().setAll(extFilter); return fileChooser.showOpenDialog(stage); } + private File chooseFileToSave(Stage stage, String description, String extension, String initialName) { + fileChooser.setTitle("Save Feature To File"); + fileChooser.setInitialDirectory(workingDir); + FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("*.feature files", "*.feature"); + fileChooser.getExtensionFilters().setAll(extFilter); + fileChooser.setInitialFileName(initialName); + return fileChooser.showSaveDialog(stage); + } + void initUi(File file, String envString, Stage stage) { AppSession session = new AppSession(file, envString); rootPane.setTop(session.headerPanel); rootPane.setCenter(session.featurePanel); rootPane.setRight(session.varsPanel); rootPane.setBottom(session.logPanel); + initNewFileAction(session.headerPanel, envString, stage); initFileOpenAction(session.headerPanel, envString, stage); + initFileSaveAction(session, envString, stage); initDirectoryOpenAction(session.headerPanel, envString, stage); initImportOpenAction(session.headerPanel, envString, stage); workingDir = file.getParentFile(); @@ -79,11 +102,39 @@ void initUi(File file, String envString, Stage stage) { private void initFileOpenAction(HeaderPanel header, String envString, Stage stage) { header.setFileOpenAction(e -> { - if(rootPane.getLeft() != null) { + if (rootPane.getLeft() != null) { rootPane.setLeft(null); } File file = chooseFile(stage, "*.feature files", "*.feature"); initUi(file, envString, stage); + needsNameToSave = false; + }); + } + + private void initNewFileAction(HeaderPanel header, String envString, Stage stage) { + header.setNewFileAction(e -> { + if (rootPane.getLeft() != null) { + rootPane.setLeft(null); + } + initUi(new File(initializeNoNameFeature()), envString, stage); + }); + } + + private void initFileSaveAction(AppSession session, String envString, Stage stage) { + session.headerPanel.setFileSaveAction(e -> { + File file; + FeatureWrapper feature = session.getFeature(); + if (needsNameToSave) { + String suggestedName = feature.getFeature().getGherkinFeature().getName(); + file = chooseFileToSave(stage, "*.feature files", "*.feature", suggestedName); + } else { + file = new File(feature.getPath()); + } + FileUtils.writeToFile(file, feature.getText()); + if (needsNameToSave) { + needsNameToSave = false; + initUi(file, envString, stage); + } }); } @@ -115,30 +166,34 @@ private void initImportOpenAction(HeaderPanel header, String envString, Stage st String json = FileUtils.toString(file); List items = ConvertUtils.readPostmanJson(json); String featureText = ConvertUtils.toKarateFeature(file.getName(), items); - String featurePath = FileUtils.replaceFileExtension(file.getPath(), "feature"); - File featureFile = new File(featurePath); - FileUtils.writeToFile(featureFile, featureText); - initUi(featureFile, envString, stage); + File noNameFeature = new File(workingDir, DEFAULT_FEATURE_NAME); + FileUtils.writeToFile(noNameFeature, featureText); + needsNameToSave = true; + initUi(noNameFeature, envString, stage); }); } + + private String initializeNoNameFeature() { + needsNameToSave = true; + File noNameFeature = new File(workingDir, DEFAULT_FEATURE_NAME); + FileUtils.writeToFile(noNameFeature, DEFAULT_FEATURE_TEXT); + return noNameFeature.getPath(); + } @Override public void start(Stage stage) throws Exception { + String fileName = null; List params = getParameters().getUnnamed(); String envString = System.getProperty(ScriptBindings.KARATE_ENV); if (!params.isEmpty()) { - String fileName = params.get(0); + fileName = params.get(0); if (params.size() > 1) { envString = params.get(1); } - initUi(new File(fileName), envString, stage); } else { - HeaderPanel header = new HeaderPanel(); - rootPane.setTop(header); - initFileOpenAction(header, envString, stage); - initDirectoryOpenAction(header, envString, stage); - initImportOpenAction(header, envString, stage); + fileName = initializeNoNameFeature(); } + initUi(new File(fileName), envString, stage); Scene scene = new Scene(rootPane, 900, 750); stage.setScene(scene); diff --git a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java index 24a7cf4f9..de45e0482 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java @@ -142,6 +142,11 @@ public void replace(StepWrapper step, String text) { headerPanel.initTextContent(); } + public void replaceFeature(String text) { + feature = feature.replaceText(text); + featurePanel.refresh(); + } + public ObservableList getVars() { if (backend.getStepDefs() == null) { return FXCollections.emptyObservableList(); diff --git a/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java b/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java index 5d62f7899..f5f0b1358 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java @@ -86,4 +86,10 @@ public void action(AppAction action) { } } + public void refresh() { + sectionPanels.clear(); + content.getChildren().clear(); + addSections(); + } + } diff --git a/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java index cf4e3c277..5b73b7751 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java @@ -47,7 +47,9 @@ public class HeaderPanel extends BorderPane { private final HBox content; private final AppSession session; + private final MenuItem newFileMenuItem; private final MenuItem openFileMenuItem; + private final MenuItem saveFileMenuItem; private final MenuItem openDirectoryMenuItem; private final MenuItem openImportMenuItem; private final TextArea textContent; @@ -76,11 +78,12 @@ public HeaderPanel(AppSession session) { }); MenuBar menuBar = new MenuBar(); Menu fileMenu = new Menu("File"); + newFileMenuItem = new MenuItem("New"); openFileMenuItem = new MenuItem("Open"); - fileMenu.getItems().addAll(openFileMenuItem); - + saveFileMenuItem = new MenuItem("Save"); openDirectoryMenuItem = new MenuItem("Load Directory"); - fileMenu.getItems().addAll(openDirectoryMenuItem); + fileMenu.getItems().addAll(newFileMenuItem, openFileMenuItem, + openDirectoryMenuItem, saveFileMenuItem); Menu importMenu = new Menu("Import"); openImportMenuItem = new MenuItem("Open"); @@ -113,11 +116,19 @@ public HeaderPanel(AppSession session) { private String getContentButtonText(boolean visible) { return visible ? "Hide Raw" : "Show Raw"; } + + public void setNewFileAction(EventHandler handler) { + newFileMenuItem.setOnAction(handler); + } public void setFileOpenAction(EventHandler handler) { openFileMenuItem.setOnAction(handler); } + public void setFileSaveAction(EventHandler handler) { + saveFileMenuItem.setOnAction(handler); + } + public void setDirectoryOpenAction(EventHandler handler) { openDirectoryMenuItem.setOnAction(handler); } @@ -134,12 +145,11 @@ public void initTextContent() { public void rebuildFeatureIfTextChanged() { String newText = textContent.getText(); if (!newText.equals(oldText)) { - Alert alert = new Alert(AlertType.INFORMATION); - alert.setHeaderText("Read Only"); - alert.setTitle("Not Implemented"); - alert.setContentText("Raw text editing is not supported."); - alert.show(); - textContent.setText(oldText); + try { + session.replaceFeature(newText); + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java b/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java index 914ac2658..9a90243af 100644 --- a/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java +++ b/karate-core/src/test/java/com/intuit/karate/cucumber/CucumberUtilsTest.java @@ -99,7 +99,7 @@ public void testInsert() { fw = fw.addLine(9, "Then assert 2 == 2"); List lines = fw.getLines(); printLines(lines); - assertEquals(16, lines.size()); + assertEquals(17, lines.size()); assertEquals(1, fw.getSections().size()); } @@ -115,7 +115,7 @@ public void testEdit() { fw = fw.replaceLines(line, line, "Then assert 2 == 2"); List lines = fw.getLines(); printLines(lines); - assertEquals(15, lines.size()); + assertEquals(16, lines.size()); assertEquals(1, fw.getSections().size()); } @@ -130,7 +130,7 @@ public void testMultiLineEdit() { fw = fw.replaceStep(step, "Then assert 2 == 2"); List lines = fw.getLines(); printLines(lines); - assertEquals(12, lines.size()); + assertEquals(13, lines.size()); assertEquals("# another comment", fw.getLines().get(9)); assertEquals("Then assert 2 == 2", fw.getLines().get(10)); assertEquals("Then match b == { foo: 'bar'}", fw.getLines().get(11)); From f681decc5afc555d638095a404aa1850c55d28a8 Mon Sep 17 00:00:00 2001 From: Venu Madhav Chukkapalli Date: Thu, 2 Aug 2018 22:48:38 +0530 Subject: [PATCH 16/23] Issue-479-1: UI enhancements to handle runAll / runUpto using JavaFX Service/Task --- .../java/com/intuit/karate/ui/AppSession.java | 35 +++++- .../com/intuit/karate/ui/FeaturePanel.java | 5 + .../com/intuit/karate/ui/HeaderPanel.java | 3 +- .../java/com/intuit/karate/ui/RunService.java | 89 ++++++++++++++ .../com/intuit/karate/ui/ScenarioPanel.java | 5 + .../com/intuit/karate/ui/SectionPanel.java | 7 +- .../java/com/intuit/karate/ui/StepPanel.java | 22 ++-- .../java/com/intuit/karate/ui/ThreadTest.java | 109 ++++++++++++++++++ .../com/intuit/karate/ui/threadtest.feature | 22 ++++ 9 files changed, 282 insertions(+), 15 deletions(-) create mode 100644 karate-core/src/main/java/com/intuit/karate/ui/RunService.java create mode 100644 karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java create mode 100644 karate-core/src/test/java/com/intuit/karate/ui/threadtest.feature diff --git a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java index de45e0482..26b064aa0 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java @@ -40,6 +40,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; + +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -56,6 +60,10 @@ public class AppSession { public final FeaturePanel featurePanel; public final VarsPanel varsPanel; public final LogPanel logPanel; + + RunService runner; + BooleanBinding runningNow; + BooleanProperty notRunning; public FeatureWrapper getFeature() { return feature; @@ -80,10 +88,16 @@ public void resetAll(String env) { } public void runAll() { - try { - featurePanel.action(AppAction.RUN); - } catch (StepException se) { - backend.getEnv().logger.error("step execution paused."); + synchronized (notRunning) { + notRunning.setValue(false); + runner.runUptoStep(null); + } + } + + public void runUpto(StepPanel stepPanel) { + synchronized (notRunning) { + notRunning.setValue(false); + runner.runUptoStep(stepPanel); } } @@ -95,6 +109,9 @@ public AppSession(File featureFile, String envString, boolean test) { CallContext callContext = new CallContext(null, true); backend = CucumberUtils.getBackendWithGlue(feature, callContext); if (!test) { + notRunning = new SimpleBooleanProperty(Boolean.TRUE); + runningNow = notRunning.not(); + runner = new RunService(this); headerPanel = new HeaderPanel(this); featurePanel = new FeaturePanel(this); varsPanel = new VarsPanel(this); @@ -159,4 +176,14 @@ public ObservableList getVars() { return FXCollections.observableList(list); } + public BooleanBinding isRunningNow() { + return runningNow; + } + + public void markRunStopped() { + synchronized (notRunning) { + notRunning.setValue(true); + } + } + } diff --git a/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java b/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java index f5f0b1358..6bdcf6ba1 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/FeaturePanel.java @@ -92,4 +92,9 @@ public void refresh() { addSections(); } + // only needed for our unit tests + SectionPanel getSectionAtIndex(int index) { + return sectionPanels.get(index); + } + } diff --git a/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java index 5b73b7751..ea60894e6 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/HeaderPanel.java @@ -27,8 +27,6 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Menu; @@ -100,6 +98,7 @@ public HeaderPanel(AppSession session) { Button envButton = new Button("Reset"); envButton.setOnAction(e -> session.resetAll(envTextField.getText())); Button runAllButton = new Button("Run ►►"); + runAllButton.disableProperty().bind(session.isRunningNow()); runAllButton.setOnAction(e -> session.runAll()); Button showContentButton = new Button(getContentButtonText(false)); initTextContent(); diff --git a/karate-core/src/main/java/com/intuit/karate/ui/RunService.java b/karate-core/src/main/java/com/intuit/karate/ui/RunService.java new file mode 100644 index 000000000..1b815f897 --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/ui/RunService.java @@ -0,0 +1,89 @@ +/* + * The MIT License + * + * Copyright 2017 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.ui; + +import javafx.concurrent.Service; +import javafx.concurrent.Task; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author vmchukky + */ +public class RunService extends Service { + + AppSession session; + StepPanel runUptoStep; + ExecutorService singleThreadExecutor; + static final String RUN_ALL_THREAD_NAME = "Karate-UI RunAll"; + static final String RUN_UPTO_THREAD_NAME_PREFIX = "Karate-UI RunUpto Step-"; + + public RunService(AppSession session) { + this.session = session; + this.runUptoStep = runUptoStep; + singleThreadExecutor = Executors.newSingleThreadExecutor(); + setExecutor(singleThreadExecutor); + } + + // passing null for runUptoStep executes complete feature + public void runUptoStep(StepPanel runUptoStep) { + // feels like a hack to pass parameters to Task (there must be some-other/better way) + this.runUptoStep = runUptoStep; + reset(); + restart(); + } + + @Override + protected Task createTask() { + return new RunTask(session, runUptoStep); + } + + static class RunTask extends Task { + final AppSession session; + final StepPanel runUptoStep; + + public RunTask(AppSession session, StepPanel runUptoStep) { + this.session = session; + this.runUptoStep = runUptoStep; + } + + @Override + protected Void call() throws Exception { + try { + if (runUptoStep != null) { + Thread.currentThread().setName(RUN_UPTO_THREAD_NAME_PREFIX + (runUptoStep.getStepIndex() + 1)); + runUptoStep.runAllUpto(); + } else { + Thread.currentThread().setName(RUN_ALL_THREAD_NAME); + session.featurePanel.action(AppAction.RUN); + } + } catch (Exception e) { + session.backend.getEnv().logger.error("step execution paused."); + } + session.markRunStopped(); + return null; + } + } +} \ No newline at end of file diff --git a/karate-core/src/main/java/com/intuit/karate/ui/ScenarioPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/ScenarioPanel.java index 2485283ae..686d96713 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/ScenarioPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/ScenarioPanel.java @@ -70,5 +70,10 @@ public void action(AppAction action) { panel.action(action); } } + + // only needed for our unit tests + StepPanel getStepAtIndex(int index) { + return stepPanels.get(index); + } } diff --git a/karate-core/src/main/java/com/intuit/karate/ui/SectionPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/SectionPanel.java index 7f82f93cc..8b3019f70 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/SectionPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/SectionPanel.java @@ -89,5 +89,10 @@ public void action(AppAction action) { scenarioPanel.action(action); } } - + + // only needed for our unit tests + StepPanel getStepAtIndex(int index) { + return (section.isOutline() ? null : scenarioPanel.getStepAtIndex(index)); + } + } diff --git a/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java index cf88e1d02..7b8c243c8 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java @@ -78,14 +78,15 @@ public StepPanel(AppSession session, StepWrapper step, Optional previ }); this.step = step; initTextArea(); + runButton.disableProperty().bind(session.isRunningNow()); runButton.setOnAction(e -> run()); - if(step.isHttpCall()) { + if (step.isHttpCall()) { BorderPane borderPane = new BorderPane(); borderPane.setPadding(new Insets(5, 0, 0, 0)); borderPane.setStyle(STYLE_HTTP_METHOD); AnchorPane anchorPane = new AnchorPane(); setUpTextAndRunButtons(previousPanel, anchorPane.getChildren(), anchorPane); - rawRequestResponse = Optional.of(new TextArea()); + rawRequestResponse = Optional.of(new TextArea()); TitledPane titledPane = new TitledPane("View raw Request/Response", rawRequestResponse.get()); Accordion accordion = new Accordion(); accordion.setPadding(new Insets(5, 5, 5, 5)); @@ -117,11 +118,12 @@ private void setUpTextAndRunButtons(Optional previousPanel, Observabl } private void setUpRunAllUptoButton(Optional previousPanel, ObservableList children, AnchorPane anchorPane) { - if(previousPanel.isPresent()) { + if (previousPanel.isPresent()) { final Button button = new Button("►►"); + button.disableProperty().bind(session.isRunningNow()); runAllUptoButton = Optional.of(button); button.setTooltip(new Tooltip("Run all steps upto current step")); - button.setOnAction(e -> runAllUpto()); + button.setOnAction(e -> session.runUpto(this)); children.add(button); anchorPane.setRightAnchor(button, 32.0); anchorPane.setTopAnchor(button, 2.0); @@ -142,7 +144,7 @@ private void run() { pass = result.isPass(); initStyleColor(); session.refreshVarsTable(); - rawRequestResponse.ifPresent( r -> updateRawRequestResponse(r)); + rawRequestResponse.ifPresent(r -> updateRawRequestResponse(r)); if (!pass) { throw new StepException(result); } @@ -150,11 +152,11 @@ private void run() { private void updateRawRequestResponse(TextArea textArea) { StringBuilder text = new StringBuilder(); - text.append("Request "+System.lineSeparator()); + text.append("Request " + System.lineSeparator()); session.getVars().stream().filter(v -> v.getName().contains(ScriptValueMap.VAR_REQUEST)) .forEach(v -> text.append(getLine(v))); text.append(System.lineSeparator()); - text.append("Response "+System.lineSeparator()); + text.append("Response " + System.lineSeparator()); session.getVars().stream().filter(v -> v.getName().contains(ScriptValueMap.VAR_RESPONSE)) .forEach(v -> text.append(getLine(v))); textArea.setText(text.toString()); @@ -164,7 +166,7 @@ private String getLine(Var var) { return var.getName() + " : " + (var.getValue() != null ? var.getValue().getAsPrettyString() : "") + System.lineSeparator(); } - private void runAllUpto() { + void runAllUpto() { previousPanel.ifPresent(p -> p.runAllUpto()); run(); } @@ -229,4 +231,8 @@ private void initTextArea() { initStyleColor(); } + public int getStepIndex() { + return step.getIndex(); + } + } diff --git a/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java b/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java new file mode 100644 index 000000000..babeb8f6c --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java @@ -0,0 +1,109 @@ +/* + * The MIT License + * + * Copyright 2017 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.ui; + +import com.sun.javafx.application.PlatformImpl; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.embed.swing.JFXPanel; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @author vmchukky + */ +public class ThreadTest { + + private static final Logger logger = LoggerFactory.getLogger(ThreadTest.class); + + @Test + public void testRunAll() { + JavaFxRunnable runnable = new JavaFxRunnable(); + new Thread(runnable).start(); + File tempFile = new File("src/test/java/com/intuit/karate/ui/threadtest.feature"); + AppSession session = new AppSession(tempFile, null, false); + session.runAll(); + assertThreadName(session, RunService.RUN_ALL_THREAD_NAME); + runnable.stopFx(); + } + + @Test + public void testRunUpto() { + JavaFxRunnable runnable = new JavaFxRunnable(); + new Thread(runnable).start(); + File tempFile = new File("src/test/java/com/intuit/karate/ui/threadtest.feature"); + AppSession session = new AppSession(tempFile, null, false); + StepPanel step10Scenario2 = session.featurePanel.getSectionAtIndex(1).getStepAtIndex(9); + session.runUpto(step10Scenario2); + assertThreadName(session, RunService.RUN_UPTO_THREAD_NAME_PREFIX + 10); + } + + private void assertThreadName(AppSession session, String expectedThreadName) { + // simplest way to wait for runAll to finish + while (session.isRunningNow().get()) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + ObservableList vars = session.getVars(); + String threadName = null; + for (Var var : vars) { + if ("threadName".equals(var.getName())) { + threadName = var.getValue().getAsString(); + break; + } + } + Assert.assertTrue(expectedThreadName.equals(threadName)); + } + + class JavaFxRunnable implements Runnable { + + final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run() { + PlatformImpl.startup(() -> { + }); + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void stopFx() { + latch.countDown(); + PlatformImpl.exit(); + } + } + +} diff --git a/karate-core/src/test/java/com/intuit/karate/ui/threadtest.feature b/karate-core/src/test/java/com/intuit/karate/ui/threadtest.feature new file mode 100644 index 000000000..05b69a0da --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/ui/threadtest.feature @@ -0,0 +1,22 @@ +Feature: Thread Test + +Scenario: Scenario-1 +* print "Scenario-1 Started" +* string a = 'a' +* string b = 'b' +* string c = 'c' +* print "Scenario-1 Finished" + +Scenario: Scenario-2 +* print "Scenario-2 Started" +* string x = 'x' +* def Thread = Java.type('java.lang.Thread') +# def sleep = Thread.sleep(3000) +* def threadName = Thread.currentThread().getName() +* match threadName contains 'Karate-UI Run' +* print 'current thread is ' + threadName +* string y = 'y' +* string z = 'z' +# def sleep = Thread.sleep(3000) +* string a = 'a' +* print "Scenario-2 Finished" \ No newline at end of file From 3fd6e693dc1a93412aca8816fd2e3821ec5647e2 Mon Sep 17 00:00:00 2001 From: Venu Madhav Chukkapalli Date: Thu, 2 Aug 2018 23:04:21 +0530 Subject: [PATCH 17/23] Disable JavaFX UI tests that need a DISPLAY (can't run in headless mode) --- .../src/test/java/com/intuit/karate/ui/ThreadTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java b/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java index babeb8f6c..f43967856 100644 --- a/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java +++ b/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java @@ -43,7 +43,7 @@ public class ThreadTest { private static final Logger logger = LoggerFactory.getLogger(ThreadTest.class); - @Test + //@Test public void testRunAll() { JavaFxRunnable runnable = new JavaFxRunnable(); new Thread(runnable).start(); @@ -54,7 +54,7 @@ public void testRunAll() { runnable.stopFx(); } - @Test + //@Test public void testRunUpto() { JavaFxRunnable runnable = new JavaFxRunnable(); new Thread(runnable).start(); From f740bca9f4106941bbb628f7c8a1ef062b9c1262 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Thu, 2 Aug 2018 23:14:38 +0530 Subject: [PATCH 18/23] renaming test to not run in ci but easier in dev --- .../karate/ui/{ThreadTest.java => ThreadTester.java} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename karate-core/src/test/java/com/intuit/karate/ui/{ThreadTest.java => ThreadTester.java} (98%) diff --git a/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java b/karate-core/src/test/java/com/intuit/karate/ui/ThreadTester.java similarity index 98% rename from karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java rename to karate-core/src/test/java/com/intuit/karate/ui/ThreadTester.java index f43967856..2e2a41fe5 100644 --- a/karate-core/src/test/java/com/intuit/karate/ui/ThreadTest.java +++ b/karate-core/src/test/java/com/intuit/karate/ui/ThreadTester.java @@ -39,11 +39,11 @@ /** * @author vmchukky */ -public class ThreadTest { +public class ThreadTester { - private static final Logger logger = LoggerFactory.getLogger(ThreadTest.class); + private static final Logger logger = LoggerFactory.getLogger(ThreadTester.class); - //@Test + @Test public void testRunAll() { JavaFxRunnable runnable = new JavaFxRunnable(); new Thread(runnable).start(); @@ -54,7 +54,7 @@ public void testRunAll() { runnable.stopFx(); } - //@Test + @Test public void testRunUpto() { JavaFxRunnable runnable = new JavaFxRunnable(); new Thread(runnable).start(); From 70548aee03cdbc1f908fea7d6f57365170ba7dae Mon Sep 17 00:00:00 2001 From: Matthew McGarvey Date: Thu, 2 Aug 2018 22:57:51 -0500 Subject: [PATCH 19/23] Fix dead link in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 235619d19..001bda206 100755 --- a/README.md +++ b/README.md @@ -151,8 +151,8 @@ And you don't need to create additional Java classes for any of the payloads tha | match !contains | match each | Fuzzy Matching - | contains short-cuts - | Schema Validation + | Schema Validation + | contains short-cuts From ad4049c78924cffc153ab306af06d4ee7c49c47e Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Tue, 7 Aug 2018 09:45:21 +0530 Subject: [PATCH 20/23] was not possible to over-ride content-type for url-encoded ref #488 --- .../karate/http/apache/ApacheHttpUtils.java | 9 +++++++-- .../java/com/intuit/karate/http/HttpUtils.java | 2 -- .../test/java/demo/encoding/encoding.feature | 2 +- .../src/test/java/demo/form/FormRunner.java | 11 +++++++++++ .../src/test/java/demo/form/form.feature | 12 ++++++++++++ .../test/java/demo/headers/content-type.feature | 3 +-- .../karate/http/jersey/JerseyHttpClient.java | 17 +++++++---------- .../com/intuit/karate/junit4/demos/dev.feature | 8 +++++++- 8 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 karate-demo/src/test/java/demo/form/FormRunner.java create mode 100644 karate-demo/src/test/java/demo/form/form.feature diff --git a/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpUtils.java b/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpUtils.java index f327d9117..62f8a737a 100644 --- a/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpUtils.java +++ b/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpUtils.java @@ -37,7 +37,7 @@ import java.util.Map; import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; -import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; @@ -143,7 +143,12 @@ public static HttpEntity getEntity(MultiValuedMap fields, String mediaType, Char if (cs == null) { cs = charset; } - return new UrlEncodedFormEntity(list, cs); + String raw = URLEncodedUtils.format(list, cs); + int pos = mediaType.indexOf(';'); + if (pos != -1) { // strip out charset param from content-type + mediaType = mediaType.substring(0, pos); + } + return new StringEntity(raw, ContentType.create(mediaType, cs)); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java b/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java index 1829cd33b..a43431b5f 100644 --- a/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java @@ -10,7 +10,6 @@ import com.intuit.karate.StringUtils; import com.intuit.karate.XmlUtils; import static com.intuit.karate.http.HttpClient.*; -import com.jayway.jsonpath.DocumentContext; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.HttpCookie; @@ -30,7 +29,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; -import org.w3c.dom.Document; /** * diff --git a/karate-demo/src/test/java/demo/encoding/encoding.feature b/karate-demo/src/test/java/demo/encoding/encoding.feature index fb55f40dc..611742120 100644 --- a/karate-demo/src/test/java/demo/encoding/encoding.feature +++ b/karate-demo/src/test/java/demo/encoding/encoding.feature @@ -64,7 +64,7 @@ Scenario: french & german form field Given url demoBaseUrl And path 'echo', 'message' And form field text = 'oliàèôç Müller' - And header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8' + And header Content-Type = 'application/x-www-form-urlencoded' When method post Then status 200 And match response == 'oliàèôç Müller' diff --git a/karate-demo/src/test/java/demo/form/FormRunner.java b/karate-demo/src/test/java/demo/form/FormRunner.java new file mode 100644 index 000000000..177116e30 --- /dev/null +++ b/karate-demo/src/test/java/demo/form/FormRunner.java @@ -0,0 +1,11 @@ +package demo.form; + +import demo.TestBase; + +/** + * + * @author pthomas3 + */ +public class FormRunner extends TestBase { + +} diff --git a/karate-demo/src/test/java/demo/form/form.feature b/karate-demo/src/test/java/demo/form/form.feature new file mode 100644 index 000000000..4f621a027 --- /dev/null +++ b/karate-demo/src/test/java/demo/form/form.feature @@ -0,0 +1,12 @@ +@apache +Feature: test url-encoded form-field submissions + +Scenario: should be able to over-ride the content-type + Given url demoBaseUrl + And path 'search', 'headers' + And form field text = 'hello' + And header Content-Type = 'application/json' + When method post + Then status 200 + And match response['content-type'][0] contains 'application/json' + diff --git a/karate-demo/src/test/java/demo/headers/content-type.feature b/karate-demo/src/test/java/demo/headers/content-type.feature index 56ddf8ace..b4cc4147e 100644 --- a/karate-demo/src/test/java/demo/headers/content-type.feature +++ b/karate-demo/src/test/java/demo/headers/content-type.feature @@ -20,8 +20,7 @@ Scenario: form post with charset When method post Then status 200 * def temp = response['content-type'][0].toLowerCase() - * assert temp.contains('application/x-www-form-urlencoded;') - * assert temp.contains('charset=utf-8') + * assert temp.contains('application/x-www-form-urlencoded') Scenario: json post with with charset and version Given path 'search', 'headers' diff --git a/karate-jersey/src/main/java/com/intuit/karate/http/jersey/JerseyHttpClient.java b/karate-jersey/src/main/java/com/intuit/karate/http/jersey/JerseyHttpClient.java index b0a33b96c..cb639a1da 100644 --- a/karate-jersey/src/main/java/com/intuit/karate/http/jersey/JerseyHttpClient.java +++ b/karate-jersey/src/main/java/com/intuit/karate/http/jersey/JerseyHttpClient.java @@ -163,7 +163,13 @@ public Entity getEntity(MultiValuedMap fields, String mediaType) { for (Entry entry : fields.entrySet()) { map.put(entry.getKey(), entry.getValue()); } - return Entity.entity(map, getMediaType(mediaType)); + // special handling, charset is not valid in content-type header here + int pos = mediaType.indexOf(';'); + if (pos != -1) { + mediaType = mediaType.substring(0, pos); + } + MediaType mt = MediaType.valueOf(mediaType); + return Entity.entity(map, mt); } @Override @@ -181,15 +187,6 @@ public Entity getEntity(List items, String mediaType) { ct = HttpUtils.getContentType(sv); } MediaType itemType = MediaType.valueOf(ct); - if (HttpUtils.isPrintable(ct)) { - Charset cs = HttpUtils.parseContentTypeCharset(mediaType); - if (cs == null) { - cs = charset; - } - if (cs != null) { - itemType = itemType.withCharset(cs.name()); - } - } if (name == null) { // most likely multipart/mixed BodyPart bp = new BodyPart().entity(sv.getAsString()).type(itemType); multiPart.bodyPart(bp); diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature index 065ae7ab2..be08b2b41 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature @@ -1,4 +1,10 @@ @ignore Feature: scratch pad to work on only one construct at a time -Scenario: test + Scenario: test + Given url 'https://httpbin.org' + And path '/post' + And form field test = '123' + And header Content-Type = 'application/json' + When method post + Then status 200 From 85d9e9fb71b24a7474f20f52fddcfb166eed3cc9 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Tue, 7 Aug 2018 09:47:19 +0530 Subject: [PATCH 21/23] removing scratchpad code --- .../test/java/com/intuit/karate/junit4/demos/dev.feature | 6 ------ 1 file changed, 6 deletions(-) diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature index be08b2b41..0c428f9a5 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/dev.feature @@ -2,9 +2,3 @@ Feature: scratch pad to work on only one construct at a time Scenario: test - Given url 'https://httpbin.org' - And path '/post' - And form field test = '123' - And header Content-Type = 'application/json' - When method post - Then status 200 From 72d56c6efee16260e9cec4ca916f0715b087458b Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Wed, 8 Aug 2018 16:05:40 +0530 Subject: [PATCH 22/23] adding regression test for #491 --- .../src/test/java/demo/upload/upload-image.feature | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/karate-demo/src/test/java/demo/upload/upload-image.feature b/karate-demo/src/test/java/demo/upload/upload-image.feature index 138327ab6..d315335bc 100644 --- a/karate-demo/src/test/java/demo/upload/upload-image.feature +++ b/karate-demo/src/test/java/demo/upload/upload-image.feature @@ -34,3 +34,13 @@ Scenario: upload image - binary request body Then status 200 And match response == read('karate-logo.jpg') And match header Content-Disposition contains 'karate-logo.jpg' + +@mock-servlet-todo +Scenario: upload stream - content-length should be sent correctly + Given path 'search', 'headers' + And param name = 'karate-logo.jpg' + And request read('karate-logo.jpg') + When method post + Then status 200 + And match response['content-length'][0] == '13575' + From b982c8c3f971e914e8f38a3355130086ac4c9be4 Mon Sep 17 00:00:00 2001 From: Venu Madhav Chukkapalli Date: Sat, 11 Aug 2018 13:02:55 +0530 Subject: [PATCH 23/23] Issue-482: show (focused) step specific vars, request/response info --- .../main/java/com/intuit/karate/ui/App.java | 8 +- .../java/com/intuit/karate/ui/AppSession.java | 28 +++--- .../java/com/intuit/karate/ui/HttpPanel.java | 79 +++++++++++++++ .../java/com/intuit/karate/ui/LogPanel.java | 15 +-- .../java/com/intuit/karate/ui/StepPanel.java | 98 ++++++++----------- .../java/com/intuit/karate/ui/VarLists.java | 60 ++++++++++++ .../java/com/intuit/karate/ui/VarsPanel.java | 17 +++- .../com/intuit/karate/ui/ThreadTester.java | 2 +- 8 files changed, 224 insertions(+), 83 deletions(-) create mode 100644 karate-core/src/main/java/com/intuit/karate/ui/HttpPanel.java create mode 100644 karate-core/src/main/java/com/intuit/karate/ui/VarLists.java diff --git a/karate-core/src/main/java/com/intuit/karate/ui/App.java b/karate-core/src/main/java/com/intuit/karate/ui/App.java index 81f90047f..6fb07f5d1 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/App.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/App.java @@ -33,6 +33,7 @@ import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; import javafx.scene.text.Font; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; @@ -91,7 +92,12 @@ void initUi(File file, String envString, Stage stage) { rootPane.setTop(session.headerPanel); rootPane.setCenter(session.featurePanel); rootPane.setRight(session.varsPanel); - rootPane.setBottom(session.logPanel); + GridPane footerPanel = new GridPane(); + footerPanel.add(session.logPanel, 0, 0); + footerPanel.add(session.httpPanel, 1, 0); + footerPanel.setPrefHeight(180); + DragResizer.makeResizable(footerPanel, false, true, true, false); + rootPane.setBottom(footerPanel); initNewFileAction(session.headerPanel, envString, stage); initFileOpenAction(session.headerPanel, envString, stage); initFileSaveAction(session, envString, stage); diff --git a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java index 26b064aa0..cfe4a48bc 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/AppSession.java @@ -60,6 +60,7 @@ public class AppSession { public final FeaturePanel featurePanel; public final VarsPanel varsPanel; public final LogPanel logPanel; + public final HttpPanel httpPanel; RunService runner; BooleanBinding runningNow; @@ -114,13 +115,15 @@ public AppSession(File featureFile, String envString, boolean test) { runner = new RunService(this); headerPanel = new HeaderPanel(this); featurePanel = new FeaturePanel(this); - varsPanel = new VarsPanel(this); + varsPanel = new VarsPanel(this, FXCollections.emptyObservableList()); logPanel = new LogPanel(backend.getEnv().logger); + httpPanel = new HttpPanel(); } else { headerPanel = null; featurePanel = null; varsPanel = null; logPanel = null; + httpPanel = null; } } @@ -131,7 +134,13 @@ public void logVar(Var var) { } public void refreshVarsTable() { - varsPanel.refresh(); + // show session vars (last executed step) + refreshVarsTable(getVars()); + } + + public void refreshVarsTable(VarLists stepVarLists) { + varsPanel.refresh(stepVarLists); + httpPanel.refresh(stepVarLists); } public FeatureSection refresh(FeatureSection section) { @@ -164,16 +173,8 @@ public void replaceFeature(String text) { featurePanel.refresh(); } - public ObservableList getVars() { - if (backend.getStepDefs() == null) { - return FXCollections.emptyObservableList(); - } - ScriptValueMap map = backend.getStepDefs().getContext().getVars(); - List list = new ArrayList(map.size()); - for (Map.Entry entry : map.entrySet()) { - list.add(new Var(entry.getKey(), entry.getValue())); - } - return FXCollections.observableList(list); + public VarLists getVars() { + return new VarLists(backend.getStepDefs()); } public BooleanBinding isRunningNow() { @@ -186,4 +187,7 @@ public void markRunStopped() { } } + public void stepIntoFeature(StepPanel stepPanel) { + logPanel.append("TODO: stepIntoFeature coming soon to karate-ui"); + } } diff --git a/karate-core/src/main/java/com/intuit/karate/ui/HttpPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/HttpPanel.java new file mode 100644 index 000000000..503772e8d --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/ui/HttpPanel.java @@ -0,0 +1,79 @@ +/* + * The MIT License + * + * Copyright 2017 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.ui; + +import javafx.collections.ObservableList; +import javafx.scene.control.Accordion; +import javafx.scene.control.TextArea; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.BorderPane; + +import static com.intuit.karate.ui.App.PADDING_INSET; + +/** + * @author vmchukky + */ +public class HttpPanel extends BorderPane { + + private final TextArea request; + private final TextArea response; + + public HttpPanel() { + this.setPadding(PADDING_INSET); + request = new TextArea(); + response = new TextArea(); + request.setPrefColumnCount(60); + response.setPrefColumnCount(60); + request.setFont(App.getDefaultFont()); + response.setFont(App.getDefaultFont()); + TitledPane requestPane = new TitledPane("View request", request); + TitledPane responsePane = new TitledPane("View response", response); + Accordion accordion = new Accordion(); + accordion.getPanes().addAll(requestPane, responsePane); + accordion.setExpandedPane(responsePane); + setCenter(accordion); + DragResizer.makeResizable(accordion, false, false, true, true); + } + + public void refresh(VarLists varLists) { + if (varLists != null) { + request.setText(getLines(varLists.getRequestVarList())); + response.setText(getLines(varLists.getResponseVarList())); + } else { + request.clear(); + response.clear(); + } + } + + private String getLines(ObservableList vars) { + StringBuilder sb = new StringBuilder(); + for (Var var : vars) { + sb.append(var.getName()).append(" : ") + .append((var.getValue() != null ? var.getValue().getAsPrettyString() : "")) + .append(System.lineSeparator()); + } + return sb.toString(); + } + +} diff --git a/karate-core/src/main/java/com/intuit/karate/ui/LogPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/LogPanel.java index a90252eb8..d093bd74a 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/LogPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/LogPanel.java @@ -24,9 +24,11 @@ package com.intuit.karate.ui; import com.intuit.karate.Logger; +import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import static com.intuit.karate.ui.App.PADDING_INSET; @@ -41,15 +43,16 @@ public class LogPanel extends BorderPane { public LogPanel(Logger logger) { setPadding(PADDING_INSET); - VBox content = new VBox(2.0); - setCenter(content); textArea = new TextArea(); - TextAreaLogAppender appender = new TextAreaLogAppender(logger, textArea); + textArea.setPrefRowCount(40); + textArea.setPrefColumnCount(120); textArea.setFont(App.getDefaultFont()); Button clearButton = new Button("Clear Log"); - clearButton.setOnAction(e -> textArea.clear()); - content.getChildren().addAll(textArea, clearButton); - DragResizer.makeResizable(textArea, false, false, true, false); + clearButton.setOnAction(e -> textArea.clear()); + setCenter(textArea); + setBottom(clearButton); + setMargin(clearButton, new Insets(2.0, 0, 0, 0)); + DragResizer.makeResizable(textArea, false, true, true, true); } public void append(String s) { diff --git a/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java b/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java index 7b8c243c8..e00be254a 100644 --- a/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java +++ b/karate-core/src/main/java/com/intuit/karate/ui/StepPanel.java @@ -27,6 +27,9 @@ import com.intuit.karate.cucumber.CucumberUtils; import com.intuit.karate.cucumber.StepResult; import com.intuit.karate.cucumber.StepWrapper; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.scene.Node; @@ -37,6 +40,7 @@ import org.slf4j.LoggerFactory; import java.util.Optional; +import java.util.regex.Pattern; /** * @@ -49,19 +53,22 @@ public class StepPanel extends AnchorPane { private final AppSession session; private final TextArea textArea; private final Button runButton; + private Button stepIntoFeatureButton; private Optional