From 6fa68ec8dc1d1ac3ee24db96e4a5ea71c1bbdeb9 Mon Sep 17 00:00:00 2001 From: Gitea Date: Sat, 1 Sep 2018 15:24:23 +0200 Subject: [PATCH 1/2] cucumber 3 working --- pom.xml | 20 +- .../listener/ExtentCucumberFormatter.java | 348 ++++++++---------- .../cucumber/listener/Reporter.java | 4 +- .../cucumber/listener/TestSourcesModel.java | 217 +++++++++++ .../cucumber/stepdefinitions/MyStepdefs.java | 3 +- 5 files changed, 395 insertions(+), 197 deletions(-) create mode 100644 src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java diff --git a/pom.xml b/pom.xml index a4baa77..1b5bd6e 100644 --- a/pom.xml +++ b/pom.xml @@ -57,19 +57,29 @@ UTF-8 - 1.2.5 + 3.0.2 - info.cukes + io.atlassian.fugue + fugue + 4.7.1 + + + org.apache.commons + commons-lang3 + 3.8 + + + + io.cucumber cucumber-junit ${cucumber.version} test - - info.cukes + io.cucumber cucumber-testng ${cucumber.version} test @@ -90,7 +100,7 @@ - info.cukes + io.cucumber cucumber-java ${cucumber.version} provided diff --git a/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java b/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java index f8c9b28..32d7c1b 100644 --- a/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java +++ b/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java @@ -3,46 +3,183 @@ import com.aventstack.extentreports.ExtentReports; import com.aventstack.extentreports.ExtentTest; import com.aventstack.extentreports.GherkinKeyword; -import com.aventstack.extentreports.markuputils.Markup; -import com.aventstack.extentreports.markuputils.MarkupHelper; import com.aventstack.extentreports.reporter.ExtentHtmlReporter; import com.aventstack.extentreports.reporter.ExtentXReporter; import com.aventstack.extentreports.reporter.KlovReporter; import com.mongodb.MongoClientURI; -import gherkin.formatter.Formatter; -import gherkin.formatter.Reporter; -import gherkin.formatter.model.*; +import cucumber.api.PickleStepTestStep; +import cucumber.api.Result; +import cucumber.api.TestStep; +import cucumber.api.event.*; +import cucumber.api.formatter.Formatter; +import gherkin.ast.*; +import io.atlassian.fugue.Either; import java.io.File; import java.net.MalformedURLException; import java.net.URL; -import java.util.LinkedList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4; /** * A cucumber based reporting listener which generates the Extent Report */ -public class ExtentCucumberFormatter implements Reporter, Formatter { +public class ExtentCucumberFormatter implements Formatter { + private static ExtentReports extentReports; - private static ExtentHtmlReporter htmlReporter; private static KlovReporter klovReporter; - private static ThreadLocal featureTestThreadLocal = new InheritableThreadLocal<>(); - private static ThreadLocal scenarioOutlineThreadLocal = new InheritableThreadLocal<>(); - static ThreadLocal scenarioThreadLocal = new InheritableThreadLocal<>(); - private static ThreadLocal> stepListThreadLocal = - new InheritableThreadLocal<>(); - static ThreadLocal stepTestThreadLocal = new InheritableThreadLocal<>(); - private boolean scenarioOutlineFlag; + private static ExtentHtmlReporter htmlReporter; + + private TestSourcesModel testSourcesModel = new TestSourcesModel(); + + private static Map featuresByUri = new HashMap<>(); + + static ExScenario currentScenario; + + static ExStep currentStep; + + static class ExFeature { + private Feature feature; + ExtentTest test; + + private ExFeature(final Feature feature, final ExtentTest test) { + this.feature = feature; + this.test = test; + } + } + + static class ExScenario { + private Either scenario; + ExtentTest test; + + private ExScenario(final Either scenario, final ExtentTest test) { + this.scenario = scenario; + this.test = test; + } + } + + static class ExStep { + private PickleStepTestStep step; + ExtentTest test; + + private ExStep(final PickleStepTestStep step, final ExtentTest test) { + this.step = step; + this.test = test; + } + } + + @Override + public void setEventPublisher(final EventPublisher publisher) { + publisher.registerHandlerFor(TestSourceRead.class, this::handleTestSourceRead); + publisher.registerHandlerFor(TestCaseStarted.class, this::handleTestCaseStarted); + publisher.registerHandlerFor(TestStepStarted.class, this::handleTestStepStarted); + publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished); + publisher.registerHandlerFor(TestRunFinished.class, this::handleTestRunFinished); + } + + private void handleTestSourceRead(final TestSourceRead event) { + testSourcesModel.addTestSourceReadEvent(event.uri, event); + final Feature feature = testSourcesModel.getFeature(event.uri); + final ExtentTest test = extentReports.createTest(com.aventstack.extentreports.gherkin.model.Feature.class, escapeHtml4(feature.getName()), escapeHtml4(feature.getDescription())); + feature.getTags().forEach(tag -> test.assignCategory(tag.getName())); + final ExFeature exFeature = new ExFeature(feature, test); + featuresByUri.put(event.uri, exFeature); + } + + private void handleTestCaseStarted(final TestCaseStarted event) { + final String uri = event.testCase.getUri(); + final ExFeature exFeature = featuresByUri.get(uri); + if(exFeature == null) { + throw new IllegalStateException("Got no feature for test case '" + uri + "'."); + } + + final TestSourcesModel.AstNode astNode = testSourcesModel.getAstNode(uri, event.testCase.getLine()); + final ScenarioDefinition scenarioDefinition = TestSourcesModel.getScenarioDefinition(astNode); + try { + final ExtentTest test = exFeature.test.createNode(new GherkinKeyword(scenarioDefinition.getKeyword()), escapeHtml4(scenarioDefinition.getName()), escapeHtml4(scenarioDefinition.getName())); + Either scenario; + if (scenarioDefinition instanceof Scenario) { + scenario = Either.left((Scenario) scenarioDefinition); + } else if (scenarioDefinition instanceof ScenarioOutline) { + scenario = Either.right( (ScenarioOutline) scenarioDefinition); + } else { + throw new IllegalStateException("Unknown test case of type '" + scenarioDefinition.getClass().getCanonicalName() + "'."); + } + final List tags = scenario.fold(Scenario::getTags, ScenarioOutline::getTags); + exFeature.feature.getTags().forEach(tag -> test.assignCategory(tag.getName())); + tags.forEach(tag -> test.assignCategory(tag.getName())); + currentScenario = new ExScenario(scenario, test); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unknown keyword '" + scenarioDefinition.getKeyword() + "'."); + } + } + + private void handleTestStepStarted(final TestStepStarted event) { + final TestStep testStep = event.testStep; + if (!(testStep instanceof PickleStepTestStep)) { + return; + } + final PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) testStep; + final List steps = currentScenario.scenario.fold(ScenarioDefinition::getSteps, ScenarioDefinition::getSteps); + final Step foundStep = steps.stream() + .filter(step -> step.getLocation().getLine() == pickleStepTestStep.getStepLine()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find step at line " + pickleStepTestStep.getStepLine())); + + try { + final ExtentTest test = currentScenario.test.createNode(new GherkinKeyword(foundStep.getKeyword()), escapeHtml4(pickleStepTestStep.getStepText())); + currentStep = new ExStep(pickleStepTestStep, test); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unknown step keyword '" + foundStep.getKeyword() + "'."); + } + } + + private void handleTestStepFinished(final TestStepFinished finished) { + if (currentStep == null) { + return; + } + + final Result.Type status = finished.result.getStatus(); + switch (status) { + case PASSED: + currentStep.test.pass(status.firstLetterCapitalizedName()); + break; + case FAILED: + final Throwable error = finished.result.getError(); + final String errorMessage = finished.result.getErrorMessage(); + if (error != null) { + currentStep.test.error(error); + } else if (errorMessage != null) { + currentStep.test.error(errorMessage); + } else { + currentStep.test.fail(status.firstLetterCapitalizedName()); + } + break; + case PENDING: + case SKIPPED: + case AMBIGUOUS: + case UNDEFINED: + currentStep.test.skip(status.firstLetterCapitalizedName()); + break; + } + + currentStep = null; + } + + private void handleTestRunFinished(final TestRunFinished event) { + extentReports.flush(); + } public ExtentCucumberFormatter(File file) { setExtentHtmlReport(file); setExtentReport(); setKlovReport(); - stepListThreadLocal.set(new LinkedList<>()); - scenarioOutlineFlag = false; } - private static void setExtentHtmlReport(File file) { + private void setExtentHtmlReport(File file) { if (htmlReporter != null) { return; } @@ -59,7 +196,7 @@ static ExtentHtmlReporter getExtentHtmlReport() { return htmlReporter; } - private static void setExtentReport() { + private void setExtentReport() { if (extentReports != null) { return; } @@ -87,10 +224,10 @@ static ExtentReports getExtentReport() { return extentReports; } - /** - * When running cucumber tests in parallel Klov reporter should be attached only once, in order to avoid duplicate builds on klov server. - */ - private static synchronized void setKlovReport() { + // /** + // * When running cucumber tests in parallel Klov reporter should be attached only once, in order to avoid duplicate builds on klov server. + // */ + private synchronized void setKlovReport() { if (extentReports == null) { //Extent reports object not found. call setExtentReport() first return; @@ -141,169 +278,4 @@ private static synchronized void setKlovReport() { static KlovReporter getKlovReport() { return klovReporter; } - - public void syntaxError(String state, String event, List legalEvents, String uri, - Integer line) { - - } - - public void uri(String uri) { - - } - - public void feature(Feature feature) { - featureTestThreadLocal.set(getExtentReport().createTest(com.aventstack.extentreports.gherkin.model.Feature.class, feature.getName())); - ExtentTest test = featureTestThreadLocal.get(); - - for (Tag tag : feature.getTags()) { - test.assignCategory(tag.getName()); - } - } - - public void scenarioOutline(ScenarioOutline scenarioOutline) { - scenarioOutlineFlag = true; - ExtentTest node = featureTestThreadLocal.get() - .createNode(com.aventstack.extentreports.gherkin.model.ScenarioOutline.class, scenarioOutline.getName()); - scenarioOutlineThreadLocal.set(node); - } - - public void examples(Examples examples) { - ExtentTest test = scenarioOutlineThreadLocal.get(); - - String[][] data = null; - List rows = examples.getRows(); - int rowSize = rows.size(); - for (int i = 0; i < rowSize; i++) { - ExamplesTableRow examplesTableRow = rows.get(i); - List cells = examplesTableRow.getCells(); - int cellSize = cells.size(); - if (data == null) { - data = new String[rowSize][cellSize]; - } - for (int j = 0; j < cellSize; j++) { - data[i][j] = cells.get(j); - } - } - test.info(MarkupHelper.createTable(data)); - } - - public void startOfScenarioLifeCycle(Scenario scenario) { - if (scenarioOutlineFlag) { - scenarioOutlineFlag = false; - } - - ExtentTest scenarioNode; - if (scenarioOutlineThreadLocal.get() != null && scenario.getKeyword().trim() - .equalsIgnoreCase("Scenario Outline")) { - scenarioNode = - scenarioOutlineThreadLocal.get().createNode(com.aventstack.extentreports.gherkin.model.Scenario.class, scenario.getName()); - } else { - scenarioNode = - featureTestThreadLocal.get().createNode(com.aventstack.extentreports.gherkin.model.Scenario.class, scenario.getName()); - } - - for (Tag tag : scenario.getTags()) { - scenarioNode.assignCategory(tag.getName()); - } - scenarioThreadLocal.set(scenarioNode); - } - - public void background(Background background) { - - } - - public void scenario(Scenario scenario) { - - } - - public void step(Step step) { - if (scenarioOutlineFlag) { - return; - } - stepListThreadLocal.get().add(step); - } - - public void endOfScenarioLifeCycle(Scenario scenario) { - - } - - public void done() { - getExtentReport().flush(); - } - - public void close() { - - } - - public void eof() { - - } - - public void before(Match match, Result result) { - - } - - public void result(Result result) { - if (scenarioOutlineFlag) { - return; - } - - if (Result.PASSED.equals(result.getStatus())) { - stepTestThreadLocal.get().pass(Result.PASSED); - } else if (Result.FAILED.equals(result.getStatus())) { - stepTestThreadLocal.get().fail(result.getError()); - } else if (Result.SKIPPED.equals(result)) { - stepTestThreadLocal.get().skip(Result.SKIPPED.getStatus()); - } else if (Result.UNDEFINED.equals(result)) { - stepTestThreadLocal.get().skip(Result.UNDEFINED.getStatus()); - } - } - - public void after(Match match, Result result) { - - } - - public void match(Match match) { - Step step = stepListThreadLocal.get().poll(); - String data[][] = null; - if (step.getRows() != null) { - List rows = step.getRows(); - int rowSize = rows.size(); - for (int i = 0; i < rowSize; i++) { - DataTableRow dataTableRow = rows.get(i); - List cells = dataTableRow.getCells(); - int cellSize = cells.size(); - if (data == null) { - data = new String[rowSize][cellSize]; - } - for (int j = 0; j < cellSize; j++) { - data[i][j] = cells.get(j); - } - } - } - - ExtentTest scenarioTest = scenarioThreadLocal.get(); - ExtentTest stepTest = null; - - try { - stepTest = scenarioTest.createNode(new GherkinKeyword(step.getKeyword()), step.getKeyword() + step.getName()); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - - if (data != null) { - Markup table = MarkupHelper.createTable(data); - stepTest.info(table); - } - - stepTestThreadLocal.set(stepTest); - } - - public void embedding(String mimeType, byte[] data) { - - } - - public void write(String text) { - - } } diff --git a/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java b/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java index d736269..6cbf47b 100644 --- a/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java +++ b/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java @@ -159,10 +159,10 @@ public static void assignAuthor(String... authorName) { } private static ExtentTest getCurrentStep() { - return ExtentCucumberFormatter.stepTestThreadLocal.get(); + return ExtentCucumberFormatter.currentStep.test; } private static ExtentTest getCurrentScenario() { - return ExtentCucumberFormatter.scenarioThreadLocal.get(); + return ExtentCucumberFormatter.currentScenario.test; } } diff --git a/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java b/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java new file mode 100644 index 0000000..e646538 --- /dev/null +++ b/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java @@ -0,0 +1,217 @@ +package com.vimalselvam.cucumber.listener; + +import cucumber.api.event.TestSourceRead; +import gherkin.AstBuilder; +import gherkin.GherkinDialect; +import gherkin.GherkinDialectProvider; +import gherkin.Parser; +import gherkin.ParserException; +import gherkin.TokenMatcher; +import gherkin.ast.Background; +import gherkin.ast.Examples; +import gherkin.ast.Feature; +import gherkin.ast.GherkinDocument; +import gherkin.ast.Node; +import gherkin.ast.ScenarioDefinition; +import gherkin.ast.ScenarioOutline; +import gherkin.ast.Step; +import gherkin.ast.TableRow; + +import java.util.HashMap; +import java.util.Map; + +/** + * Shameless copy from cucumber.runtime.formatter.TestSourcesModel + */ +final class TestSourcesModel { + private final Map pathToReadEventMap = new HashMap(); + private final Map pathToAstMap = new HashMap(); + private final Map> pathToNodeMap = new HashMap>(); + + static Feature getFeatureForTestCase(AstNode astNode) { + while (astNode.parent != null) { + astNode = astNode.parent; + } + return (Feature) astNode.node; + } + + static Background getBackgroundForTestCase(AstNode astNode) { + Feature feature = getFeatureForTestCase(astNode); + ScenarioDefinition backgound = feature.getChildren().get(0); + if (backgound instanceof Background) { + return (Background) backgound; + } else { + return null; + } + } + + static ScenarioDefinition getScenarioDefinition(AstNode astNode) { + return astNode.node instanceof ScenarioDefinition ? (ScenarioDefinition) astNode.node : (ScenarioDefinition) astNode.parent.parent.node; + } + + static boolean isScenarioOutlineScenario(AstNode astNode) { + return !(astNode.node instanceof ScenarioDefinition); + } + + static boolean isBackgroundStep(AstNode astNode) { + return astNode.parent.node instanceof Background; + } + + static String calculateId(AstNode astNode) { + Node node = astNode.node; + if (node instanceof ScenarioDefinition) { + return calculateId(astNode.parent) + ";" + convertToId(((ScenarioDefinition) node).getName()); + } + if (node instanceof ExamplesRowWrapperNode) { + return calculateId(astNode.parent) + ";" + Integer.toString(((ExamplesRowWrapperNode) node).bodyRowIndex + 2); + } + if (node instanceof TableRow) { + return calculateId(astNode.parent) + ";" + Integer.toString(1); + } + if (node instanceof Examples) { + return calculateId(astNode.parent) + ";" + convertToId(((Examples) node).getName()); + } + if (node instanceof Feature) { + return convertToId(((Feature) node).getName()); + } + return ""; + } + + static String convertToId(String name) { + return name.replaceAll("[\\s'_,!]", "-").toLowerCase(); + } + + void addTestSourceReadEvent(String path, TestSourceRead event) { + pathToReadEventMap.put(path, event); + } + + Feature getFeature(String path) { + if (!pathToAstMap.containsKey(path)) { + parseGherkinSource(path); + } + if (pathToAstMap.containsKey(path)) { + return pathToAstMap.get(path).getFeature(); + } + return null; + } + + ScenarioDefinition getScenarioDefinition(String path, int line) { + return getScenarioDefinition(getAstNode(path, line)); + } + + AstNode getAstNode(String path, int line) { + if (!pathToNodeMap.containsKey(path)) { + parseGherkinSource(path); + } + if (pathToNodeMap.containsKey(path)) { + return pathToNodeMap.get(path).get(line); + } + return null; + } + + boolean hasBackground(String path, int line) { + if (!pathToNodeMap.containsKey(path)) { + parseGherkinSource(path); + } + if (pathToNodeMap.containsKey(path)) { + AstNode astNode = pathToNodeMap.get(path).get(line); + return getBackgroundForTestCase(astNode) != null; + } + return false; + } + + String getKeywordFromSource(String uri, int stepLine) { + Feature feature = getFeature(uri); + if (feature != null) { + TestSourceRead event = getTestSourceReadEvent(uri); + String trimmedSourceLine = event.source.split("\n")[stepLine - 1].trim(); + GherkinDialect dialect = new GherkinDialectProvider(feature.getLanguage()).getDefaultDialect(); + for (String keyword : dialect.getStepKeywords()) { + if (trimmedSourceLine.startsWith(keyword)) { + return keyword; + } + } + } + return ""; + } + + private TestSourceRead getTestSourceReadEvent(String uri) { + if (pathToReadEventMap.containsKey(uri)) { + return pathToReadEventMap.get(uri); + } + return null; + } + + String getFeatureName(String uri) { + Feature feature = getFeature(uri); + if (feature != null) { + return feature.getName(); + } + return ""; + } + + private void parseGherkinSource(String path) { + if (!pathToReadEventMap.containsKey(path)) { + return; + } + Parser parser = new Parser(new AstBuilder()); + TokenMatcher matcher = new TokenMatcher(); + try { + GherkinDocument gherkinDocument = parser.parse(pathToReadEventMap.get(path).source, matcher); + pathToAstMap.put(path, gherkinDocument); + Map nodeMap = new HashMap(); + AstNode currentParent = new AstNode(gherkinDocument.getFeature(), null); + for (ScenarioDefinition child : gherkinDocument.getFeature().getChildren()) { + processScenarioDefinition(nodeMap, child, currentParent); + } + pathToNodeMap.put(path, nodeMap); + } catch (ParserException e) { + // Ignore exceptions + } + } + + private void processScenarioDefinition(Map nodeMap, ScenarioDefinition child, AstNode currentParent) { + AstNode childNode = new AstNode(child, currentParent); + nodeMap.put(child.getLocation().getLine(), childNode); + for (Step step : child.getSteps()) { + nodeMap.put(step.getLocation().getLine(), new AstNode(step, childNode)); + } + if (child instanceof ScenarioOutline) { + processScenarioOutlineExamples(nodeMap, (ScenarioOutline) child, childNode); + } + } + + private void processScenarioOutlineExamples(Map nodeMap, ScenarioOutline scenarioOutline, AstNode childNode) { + for (Examples examples : scenarioOutline.getExamples()) { + AstNode examplesNode = new AstNode(examples, childNode); + TableRow headerRow = examples.getTableHeader(); + AstNode headerNode = new AstNode(headerRow, examplesNode); + nodeMap.put(headerRow.getLocation().getLine(), headerNode); + for (int i = 0; i < examples.getTableBody().size(); ++i) { + TableRow examplesRow = examples.getTableBody().get(i); + Node rowNode = new ExamplesRowWrapperNode(examplesRow, i); + AstNode expandedScenarioNode = new AstNode(rowNode, examplesNode); + nodeMap.put(examplesRow.getLocation().getLine(), expandedScenarioNode); + } + } + } + + class ExamplesRowWrapperNode extends Node { + final int bodyRowIndex; + + ExamplesRowWrapperNode(Node examplesRow, int bodyRowIndex) { + super(examplesRow.getLocation()); + this.bodyRowIndex = bodyRowIndex; + } + } + + class AstNode { + final Node node; + final AstNode parent; + + AstNode(Node node, AstNode parent) { + this.node = node; + this.parent = parent; + } + } +} diff --git a/src/test/java/com/vimalselvam/cucumber/stepdefinitions/MyStepdefs.java b/src/test/java/com/vimalselvam/cucumber/stepdefinitions/MyStepdefs.java index 801d5fd..a9e93a9 100644 --- a/src/test/java/com/vimalselvam/cucumber/stepdefinitions/MyStepdefs.java +++ b/src/test/java/com/vimalselvam/cucumber/stepdefinitions/MyStepdefs.java @@ -1,7 +1,6 @@ package com.vimalselvam.cucumber.stepdefinitions; import com.vimalselvam.cucumber.listener.Reporter; -import cucumber.api.DataTable; import cucumber.api.Scenario; import cucumber.api.java.Before; import cucumber.api.java.en.Given; @@ -35,6 +34,6 @@ public void beforeScenario(Scenario scenario) { // Assert.assertTrue(false); } - @When("^I login with credentials$") public void iLoginWithCredentials(DataTable table) { + @When("^I login with credentials$") public void iLoginWithCredentials(io.cucumber.datatable.DataTable table) { } } From 5e300ad91d7b05b8ba95a2778a7b85399e3e40f3 Mon Sep 17 00:00:00 2001 From: Gitea Date: Sat, 1 Sep 2018 19:36:41 +0200 Subject: [PATCH 2/2] move state of reporter into thread local --- .../listener/ExtentCucumberFormatter.java | 207 ++++++++---------- .../cucumber/listener/Reporter.java | 5 +- .../cucumber/listener/TestSourcesModel.java | 2 +- .../listener/state/ExtentFeature.java | 22 ++ .../listener/state/ExtentScenario.java | 24 ++ .../cucumber/listener/state/ExtentState.java | 110 ++++++++++ .../cucumber/listener/state/ExtentStep.java | 22 ++ .../state/ThreadLocalExtentState.java | 24 ++ 8 files changed, 299 insertions(+), 117 deletions(-) create mode 100644 src/main/java/com/vimalselvam/cucumber/listener/state/ExtentFeature.java create mode 100644 src/main/java/com/vimalselvam/cucumber/listener/state/ExtentScenario.java create mode 100644 src/main/java/com/vimalselvam/cucumber/listener/state/ExtentState.java create mode 100644 src/main/java/com/vimalselvam/cucumber/listener/state/ExtentStep.java create mode 100644 src/main/java/com/vimalselvam/cucumber/listener/state/ThreadLocalExtentState.java diff --git a/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java b/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java index 32d7c1b..b0f76f8 100644 --- a/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java +++ b/src/main/java/com/vimalselvam/cucumber/listener/ExtentCucumberFormatter.java @@ -7,6 +7,7 @@ import com.aventstack.extentreports.reporter.ExtentXReporter; import com.aventstack.extentreports.reporter.KlovReporter; import com.mongodb.MongoClientURI; +import com.vimalselvam.cucumber.listener.state.*; import cucumber.api.PickleStepTestStep; import cucumber.api.Result; import cucumber.api.TestStep; @@ -18,9 +19,7 @@ import java.io.File; import java.net.MalformedURLException; import java.net.URL; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4; @@ -33,44 +32,6 @@ public class ExtentCucumberFormatter implements Formatter { private static KlovReporter klovReporter; private static ExtentHtmlReporter htmlReporter; - private TestSourcesModel testSourcesModel = new TestSourcesModel(); - - private static Map featuresByUri = new HashMap<>(); - - static ExScenario currentScenario; - - static ExStep currentStep; - - static class ExFeature { - private Feature feature; - ExtentTest test; - - private ExFeature(final Feature feature, final ExtentTest test) { - this.feature = feature; - this.test = test; - } - } - - static class ExScenario { - private Either scenario; - ExtentTest test; - - private ExScenario(final Either scenario, final ExtentTest test) { - this.scenario = scenario; - this.test = test; - } - } - - static class ExStep { - private PickleStepTestStep step; - ExtentTest test; - - private ExStep(final PickleStepTestStep step, final ExtentTest test) { - this.step = step; - this.test = test; - } - } - @Override public void setEventPublisher(final EventPublisher publisher) { publisher.registerHandlerFor(TestSourceRead.class, this::handleTestSourceRead); @@ -81,92 +42,110 @@ public void setEventPublisher(final EventPublisher publisher) { } private void handleTestSourceRead(final TestSourceRead event) { - testSourcesModel.addTestSourceReadEvent(event.uri, event); - final Feature feature = testSourcesModel.getFeature(event.uri); - final ExtentTest test = extentReports.createTest(com.aventstack.extentreports.gherkin.model.Feature.class, escapeHtml4(feature.getName()), escapeHtml4(feature.getDescription())); - feature.getTags().forEach(tag -> test.assignCategory(tag.getName())); - final ExFeature exFeature = new ExFeature(feature, test); - featuresByUri.put(event.uri, exFeature); + ThreadLocalExtentState.modifyState(state -> { + final TestSourcesModel testSourcesModel = state.getTestSourcesModel(); + testSourcesModel.addTestSourceReadEvent(event.uri, event); + final Feature feature = testSourcesModel.getFeature(event.uri); + final ExtentTest test = extentReports.createTest(com.aventstack.extentreports.gherkin.model.Feature.class, escapeHtml4(feature.getName()), escapeHtml4(feature.getDescription())); + feature.getTags().forEach(tag -> test.assignCategory(tag.getName())); + final ExtentFeature extentFeature = new ExtentFeature(feature, test); + state.putFeature(event.uri, extentFeature); + + return state; + }); } private void handleTestCaseStarted(final TestCaseStarted event) { - final String uri = event.testCase.getUri(); - final ExFeature exFeature = featuresByUri.get(uri); - if(exFeature == null) { - throw new IllegalStateException("Got no feature for test case '" + uri + "'."); - } - - final TestSourcesModel.AstNode astNode = testSourcesModel.getAstNode(uri, event.testCase.getLine()); - final ScenarioDefinition scenarioDefinition = TestSourcesModel.getScenarioDefinition(astNode); - try { - final ExtentTest test = exFeature.test.createNode(new GherkinKeyword(scenarioDefinition.getKeyword()), escapeHtml4(scenarioDefinition.getName()), escapeHtml4(scenarioDefinition.getName())); - Either scenario; - if (scenarioDefinition instanceof Scenario) { - scenario = Either.left((Scenario) scenarioDefinition); - } else if (scenarioDefinition instanceof ScenarioOutline) { - scenario = Either.right( (ScenarioOutline) scenarioDefinition); - } else { - throw new IllegalStateException("Unknown test case of type '" + scenarioDefinition.getClass().getCanonicalName() + "'."); + ThreadLocalExtentState.modifyState(state -> { + final String uri = event.testCase.getUri(); + final ExtentFeature extentFeature = state.getFeature(uri).orElseThrow(() -> new IllegalStateException("Got no feature for test case '" + uri + "'.")); + final TestSourcesModel.AstNode astNode = state.getTestSourcesModel().getAstNode(uri, event.testCase.getLine()); + final ScenarioDefinition scenarioDefinition = TestSourcesModel.getScenarioDefinition(astNode); + try { + final ExtentTest test = extentFeature.getTest().createNode(new GherkinKeyword(scenarioDefinition.getKeyword()), escapeHtml4(scenarioDefinition.getName()), escapeHtml4(scenarioDefinition.getName())); + Either scenario; + if (scenarioDefinition instanceof Scenario) { + scenario = Either.left((Scenario) scenarioDefinition); + } else if (scenarioDefinition instanceof ScenarioOutline) { + scenario = Either.right((ScenarioOutline) scenarioDefinition); + } else { + throw new IllegalStateException("Unknown test case of type '" + scenarioDefinition.getClass().getCanonicalName() + "'."); + } + final List tags = scenario.fold(Scenario::getTags, ScenarioOutline::getTags); + extentFeature.getFeature().getTags().forEach(tag -> test.assignCategory(tag.getName())); + tags.forEach(tag -> test.assignCategory(tag.getName())); + final ExtentScenario extentScenario = new ExtentScenario(scenario, test); + + return ExtentState.ExtentStateBuilder + .anExtentState(state) + .withCurrentFeature(extentFeature) + .withCurrentScenario(extentScenario) + .build(); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unknown keyword '" + scenarioDefinition.getKeyword() + "'."); } - final List tags = scenario.fold(Scenario::getTags, ScenarioOutline::getTags); - exFeature.feature.getTags().forEach(tag -> test.assignCategory(tag.getName())); - tags.forEach(tag -> test.assignCategory(tag.getName())); - currentScenario = new ExScenario(scenario, test); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unknown keyword '" + scenarioDefinition.getKeyword() + "'."); - } + }); + } private void handleTestStepStarted(final TestStepStarted event) { - final TestStep testStep = event.testStep; - if (!(testStep instanceof PickleStepTestStep)) { - return; - } - final PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) testStep; - final List steps = currentScenario.scenario.fold(ScenarioDefinition::getSteps, ScenarioDefinition::getSteps); - final Step foundStep = steps.stream() - .filter(step -> step.getLocation().getLine() == pickleStepTestStep.getStepLine()) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Could not find step at line " + pickleStepTestStep.getStepLine())); - - try { - final ExtentTest test = currentScenario.test.createNode(new GherkinKeyword(foundStep.getKeyword()), escapeHtml4(pickleStepTestStep.getStepText())); - currentStep = new ExStep(pickleStepTestStep, test); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unknown step keyword '" + foundStep.getKeyword() + "'."); - } + ThreadLocalExtentState.modifyState(state -> { + final TestStep testStep = event.testStep; + if (!(testStep instanceof PickleStepTestStep)) { + return state; + } + final PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) testStep; + final List steps = state.getCurrentScenario().getScenario().fold(ScenarioDefinition::getSteps, ScenarioDefinition::getSteps); + final Step foundStep = steps.stream() + .filter(step -> step.getLocation().getLine() == pickleStepTestStep.getStepLine()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find step at line " + pickleStepTestStep.getStepLine())); + + try { + final ExtentTest test = state.getCurrentScenario().getTest().createNode(new GherkinKeyword(foundStep.getKeyword()), escapeHtml4(pickleStepTestStep.getStepText())); + final ExtentStep currentStep = new ExtentStep(pickleStepTestStep, test); + + return ExtentState.ExtentStateBuilder.anExtentState(state).withCurrentStep(currentStep).build(); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unknown step keyword '" + foundStep.getKeyword() + "'."); + } + }); } private void handleTestStepFinished(final TestStepFinished finished) { - if (currentStep == null) { - return; - } + ThreadLocalExtentState.modifyState(state -> { + final ExtentStep currentStep = state.getCurrentStep(); + if (currentStep == null) { + return state; + } - final Result.Type status = finished.result.getStatus(); - switch (status) { - case PASSED: - currentStep.test.pass(status.firstLetterCapitalizedName()); - break; - case FAILED: - final Throwable error = finished.result.getError(); - final String errorMessage = finished.result.getErrorMessage(); - if (error != null) { - currentStep.test.error(error); - } else if (errorMessage != null) { - currentStep.test.error(errorMessage); - } else { - currentStep.test.fail(status.firstLetterCapitalizedName()); - } - break; - case PENDING: - case SKIPPED: - case AMBIGUOUS: - case UNDEFINED: - currentStep.test.skip(status.firstLetterCapitalizedName()); - break; - } + final Result.Type status = finished.result.getStatus(); + final ExtentTest test = currentStep.getTest(); + switch (status) { + case PASSED: + test.pass(status.firstLetterCapitalizedName()); + break; + case FAILED: + final Throwable error = finished.result.getError(); + final String errorMessage = finished.result.getErrorMessage(); + if (error != null) { + test.error(error); + } else if (errorMessage != null) { + test.error(errorMessage); + } else { + test.fail(status.firstLetterCapitalizedName()); + } + break; + case PENDING: + case SKIPPED: + case AMBIGUOUS: + case UNDEFINED: + test.skip(status.firstLetterCapitalizedName()); + break; + } - currentStep = null; + return ExtentState.ExtentStateBuilder.anExtentState(state).withCurrentStep(null).build(); + }); } private void handleTestRunFinished(final TestRunFinished event) { diff --git a/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java b/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java index 6cbf47b..95a63f0 100644 --- a/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java +++ b/src/main/java/com/vimalselvam/cucumber/listener/Reporter.java @@ -4,6 +4,7 @@ import com.aventstack.extentreports.ExtentTest; import com.aventstack.extentreports.reporter.ExtentHtmlReporter; import com.aventstack.extentreports.reporter.KlovReporter; +import com.vimalselvam.cucumber.listener.state.ThreadLocalExtentState; import java.io.File; import java.io.IOException; @@ -159,10 +160,10 @@ public static void assignAuthor(String... authorName) { } private static ExtentTest getCurrentStep() { - return ExtentCucumberFormatter.currentStep.test; + return ThreadLocalExtentState.getState().getCurrentStep().getTest(); } private static ExtentTest getCurrentScenario() { - return ExtentCucumberFormatter.currentScenario.test; + return ThreadLocalExtentState.getState().getCurrentScenario().getTest(); } } diff --git a/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java b/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java index e646538..aad3de3 100644 --- a/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java +++ b/src/main/java/com/vimalselvam/cucumber/listener/TestSourcesModel.java @@ -23,7 +23,7 @@ /** * Shameless copy from cucumber.runtime.formatter.TestSourcesModel */ -final class TestSourcesModel { +public class TestSourcesModel { private final Map pathToReadEventMap = new HashMap(); private final Map pathToAstMap = new HashMap(); private final Map> pathToNodeMap = new HashMap>(); diff --git a/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentFeature.java b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentFeature.java new file mode 100644 index 0000000..92bb112 --- /dev/null +++ b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentFeature.java @@ -0,0 +1,22 @@ +package com.vimalselvam.cucumber.listener.state; + +import com.aventstack.extentreports.ExtentTest; +import gherkin.ast.Feature; + +public class ExtentFeature { + private Feature feature; + private ExtentTest test; + + public ExtentFeature(final Feature feature, final ExtentTest test) { + this.feature = feature; + this.test = test; + } + + public Feature getFeature() { + return feature; + } + + public ExtentTest getTest() { + return test; + } +} diff --git a/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentScenario.java b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentScenario.java new file mode 100644 index 0000000..caa09b4 --- /dev/null +++ b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentScenario.java @@ -0,0 +1,24 @@ +package com.vimalselvam.cucumber.listener.state; + +import com.aventstack.extentreports.ExtentTest; +import gherkin.ast.Scenario; +import gherkin.ast.ScenarioOutline; +import io.atlassian.fugue.Either; + +public class ExtentScenario { + private Either scenario; + private ExtentTest test; + + public ExtentScenario(final Either scenario, final ExtentTest test) { + this.scenario = scenario; + this.test = test; + } + + public Either getScenario() { + return scenario; + } + + public ExtentTest getTest() { + return test; + } +} diff --git a/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentState.java b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentState.java new file mode 100644 index 0000000..3286aec --- /dev/null +++ b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentState.java @@ -0,0 +1,110 @@ +package com.vimalselvam.cucumber.listener.state; + +import com.vimalselvam.cucumber.listener.TestSourcesModel; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class ExtentState { + private ExtentFeature currentFeature; + + private ExtentScenario currentScenario; + + private ExtentStep currentStep; + + private TestSourcesModel testSourcesModel; + + private Map featuresByUri; + + public void putFeature(final String uri, final ExtentFeature extentFeature) { + this.featuresByUri.put(uri, extentFeature); + } + + public Optional getFeature(final String uri) { + return Optional.ofNullable(this.featuresByUri.get(uri)); + } + + public TestSourcesModel getTestSourcesModel() { + return testSourcesModel; + } + + public ExtentFeature getCurrentFeature() { + return currentFeature; + } + + public ExtentScenario getCurrentScenario() { + return currentScenario; + } + + public ExtentStep getCurrentStep() { + return currentStep; + } + + private ExtentState(final TestSourcesModel testSourcesModel, final Map featuresByUri) { + this.testSourcesModel = testSourcesModel; + this.featuresByUri = featuresByUri; + } + + public static final class ExtentStateBuilder { + private ExtentFeature currentFeature; + private ExtentScenario currentScenario; + private ExtentStep currentStep; + private TestSourcesModel testSourcesModel; + private Map featuresByUri; + + private ExtentStateBuilder() { + } + + public static ExtentStateBuilder anExtentState() { + final ExtentStateBuilder extentStateBuilder = new ExtentStateBuilder(); + extentStateBuilder.testSourcesModel = new TestSourcesModel(); + extentStateBuilder.featuresByUri = new ConcurrentHashMap<>(); + + return extentStateBuilder; + } + + public static ExtentStateBuilder anExtentState(final ExtentState existing) { + return anExtentState() + .withTestSourceModel(existing.testSourcesModel) + .withFeaturesByUri(existing.featuresByUri) + .withCurrentFeature(existing.currentFeature) + .withCurrentScenario(existing.currentScenario) + .withCurrentStep(existing.currentStep); + } + + private ExtentStateBuilder withTestSourceModel(final TestSourcesModel testSourceModel) { + this.testSourcesModel = testSourceModel; + return this; + } + + private ExtentStateBuilder withFeaturesByUri(final Map featureByUri) { + this.featuresByUri = featureByUri; + return this; + } + + public ExtentStateBuilder withCurrentFeature(final ExtentFeature currentFeature) { + this.currentFeature = currentFeature; + return this; + } + + public ExtentStateBuilder withCurrentScenario(final ExtentScenario currentScenario) { + this.currentScenario = currentScenario; + return this; + } + + public ExtentStateBuilder withCurrentStep(final ExtentStep currentStep) { + this.currentStep = currentStep; + return this; + } + + + public ExtentState build() { + ExtentState exState = new ExtentState(this.testSourcesModel, this.featuresByUri); + exState.currentStep = this.currentStep; + exState.currentScenario = this.currentScenario; + exState.currentFeature = this.currentFeature; + return exState; + } + } +} diff --git a/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentStep.java b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentStep.java new file mode 100644 index 0000000..ca5825c --- /dev/null +++ b/src/main/java/com/vimalselvam/cucumber/listener/state/ExtentStep.java @@ -0,0 +1,22 @@ +package com.vimalselvam.cucumber.listener.state; + +import com.aventstack.extentreports.ExtentTest; +import cucumber.api.PickleStepTestStep; + +public class ExtentStep { + private PickleStepTestStep step; + private ExtentTest test; + + public ExtentStep(final PickleStepTestStep step, final ExtentTest test) { + this.step = step; + this.test = test; + } + + public PickleStepTestStep getStep() { + return step; + } + + public ExtentTest getTest() { + return test; + } +} diff --git a/src/main/java/com/vimalselvam/cucumber/listener/state/ThreadLocalExtentState.java b/src/main/java/com/vimalselvam/cucumber/listener/state/ThreadLocalExtentState.java new file mode 100644 index 0000000..76e321a --- /dev/null +++ b/src/main/java/com/vimalselvam/cucumber/listener/state/ThreadLocalExtentState.java @@ -0,0 +1,24 @@ +package com.vimalselvam.cucumber.listener.state; + +import java.util.function.Function; + +public class ThreadLocalExtentState { + + private ThreadLocalExtentState() {} + + private static ThreadLocal state = new InheritableThreadLocal<>(); + + public static void modifyState(final Function f) { + ExtentState exState = state.get(); + if (exState == null) { + exState = ExtentState.ExtentStateBuilder.anExtentState().build(); + } + + final ExtentState newState = f.apply(exState); + state.set(newState); + } + + public static ExtentState getState() { + return state.get(); + } +}