package courgette.integration.extentreports; import com.aventstack.extentreports.ExtentReports; import com.aventstack.extentreports.ExtentTest; import com.aventstack.extentreports.GherkinKeyword; import com.aventstack.extentreports.MediaEntityBuilder; import com.aventstack.extentreports.Status; import com.aventstack.extentreports.reporter.ExtentSparkReporter; import courgette.runtime.CourgetteException; import courgette.runtime.report.model.Embedding; import courgette.runtime.report.model.Feature; import courgette.runtime.report.model.Hook; import courgette.runtime.report.model.Result; import courgette.runtime.report.model.Scenario; import courgette.runtime.report.model.Step; import courgette.runtime.report.model.Tag; import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; public class ExtentReportsBuilder { private ExtentReportsProperties extentReportsProperties; private List<Feature> featureList; private boolean isStrict; private ExtentReportsBuilder(ExtentReportsProperties extentReportsProperties, List<Feature> featureList, boolean isStrict) { this.extentReportsProperties = extentReportsProperties; this.featureList = featureList; this.isStrict = isStrict; } public static ExtentReportsBuilder create(ExtentReportsProperties extentReportProperties, List<Feature> featureList, boolean isStrict) { return new ExtentReportsBuilder(extentReportProperties, featureList, isStrict); } public void buildReport() { final ExtentSparkReporter extentSparkReporter = new ExtentSparkReporter(extentReportsProperties.getReportFilename()); if (extentReportsProperties.getXMLConfigFile() != null) { extentSparkReporter.loadXMLConfig(extentReportsProperties.getXMLConfigFile(), true); } final ExtentReports extentReports = new ExtentReports(); extentReports.setReportUsesManualConfiguration(true); extentReports.attachReporter(extentSparkReporter); getDistinctFeatureUris().forEach(featureUri -> { List<Feature> features = featureList.stream().filter(f -> f.getUri().equals(featureUri)).collect(Collectors.toList()); addFeatures(extentReports, features); }); extentReports.flush(); } private List<String> getDistinctFeatureUris() { return featureList.stream().map(Feature::getUri).distinct().collect(Collectors.toList()); } private void addFeatures(ExtentReports extentReports, List<Feature> features) { final ExtentTest featureNode = createBddTest(extentReports, features.get(0).getName()); features.forEach(feature -> { if (feature.getScenarios().size() > 1) { addFeatureStartAndEndTime(featureNode, feature); } for (Scenario scenario : feature.getScenarios()) { if (scenario.getKeyword().startsWith("Scenario")) { addScenario(featureNode, scenario); addScenarioStartAndEndTime(featureNode, scenario); } } }); } private ExtentTest createBddTest(ExtentReports extentReports, String featureName) { ExtentTest featureNode = extentReports.createTest(featureName); featureNode.getModel().setBddType(com.aventstack.extentreports.gherkin.model.Feature.class); return featureNode; } private void addScenario(ExtentTest featureNode, Scenario scenario) { final ExtentTest scenarioNode = createGherkinNode(featureNode, "Scenario", scenario.getName(), false); addBeforeOrAfterDetails(scenarioNode, scenario.getBefore()); addSteps(scenarioNode, scenario); addBeforeOrAfterDetails(scenarioNode, scenario.getAfter()); assignCategoryToScenario(scenarioNode, scenario.getTags()); } private void assignCategoryToScenario(ExtentTest scenarioNode, List<Tag> tags) { tags.forEach(tag -> scenarioNode.assignCategory(tag.getName())); } private void addSteps(ExtentTest scenarioNode, Scenario scenario) { Date startTime = getStartTime(scenario.getStartTimestamp()); List<Step> steps = scenario.getSteps(); steps.forEach(step -> { addBeforeOrAfterDetails(scenarioNode, step.getBefore()); ExtentTest stepNode = createStepNode(scenarioNode, step, startTime); addAdditionalStepDetails(stepNode, step); setStepResult(stepNode, step, isStrict); addBeforeOrAfterDetails(scenarioNode, step.getAfter()); }); } private ExtentTest createStepNode(ExtentTest scenarioNode, Step step, Date startTime) { ExtentTest stepNode = createGherkinNode(scenarioNode, step.getKeyword().trim(), step.getName(), true); stepNode.getModel().setStartTime(startTime); return stepNode; } private void addBeforeOrAfterDetails(ExtentTest scenarioNode, List<Hook> hooks) { hooks.forEach(hook -> { String error = hook.getResult().getErrorMessage(); List<String> output = hook.getOutput(); List<Embedding> embeddings = hook.getEmbeddings(); final ExtentTest hookNode = scenarioNode.createNode(com.aventstack.extentreports.gherkin.model.Asterisk.class, hook.getLocation()); addOutputs(hookNode, output); addError(hookNode, error); addImageEmbeddings(hookNode, embeddings); }); } private void addAdditionalStepDetails(ExtentTest stepNode, Step step) { String error = step.getResult().getErrorMessage(); List<String> output = step.getOutput(); List<Embedding> embeddings = step.getEmbeddings(); addOutputs(stepNode, output); addError(stepNode, error); addImageEmbeddings(stepNode, embeddings); } private void addOutputs(ExtentTest node, List<String> outputs) { outputs.forEach(output -> log(node, output)); } private void addImageEmbeddings(ExtentTest node, List<Embedding> embeddings) { embeddings.forEach(embedding -> { if (embedding.getMimeType().startsWith("image")) { addBase64ScreenCapture(node, embedding.getData()); } }); } private void addBase64ScreenCapture(ExtentTest node, String base64Image) { try { node.log(Status.INFO, "", MediaEntityBuilder.createScreenCaptureFromBase64String(base64Image).build()); } catch (IOException e) { System.err.println("[Courgette Extent Reports Plugin] Unable to embed image. Reason: " + e.getMessage()); } } private void addError(ExtentTest node, String error) { log(node, error); } private void log(ExtentTest node, String message) { if (message != null) { node.log(Status.INFO, message); } } private ExtentTest createGherkinNode(ExtentTest parent, String keyword, String name, boolean appendKeyword) { try { String nodeName = appendKeyword ? (keyword + " " + name) : name; return parent.createNode(new GherkinKeyword(keyword), nodeName); } catch (ClassNotFoundException e) { throw new CourgetteException(e); } } private void setStepResult(ExtentTest extentTest, Step step, boolean isStrict) { if (step.skipped()) { extentTest.skip(""); } else if (step.passed(isStrict)) { extentTest.pass(""); } else { extentTest.fail(""); } } private void addFeatureStartAndEndTime(ExtentTest featureNode, Feature feature) { Date featureStartTime = getEarliestStartTime(feature.getScenarios()); Date featureEndTime = getEndTime(featureStartTime.getTime(), feature.getScenarios()); addStartAndEndTime(featureNode, featureStartTime, featureEndTime); } private void addScenarioStartAndEndTime(ExtentTest scenarioNode, Scenario scenario) { List<Scenario> scenarios = new ArrayList<>(); scenarios.add(scenario); Date scenarioStartTime = getStartTime(scenario.getStartTimestamp()); Date scenarioEndTime = getEndTime(scenarioStartTime.getTime(), scenarios); addStartAndEndTime(scenarioNode, scenarioStartTime, scenarioEndTime); } private void addStartAndEndTime(ExtentTest node, Date startTime, Date endTime) { node.getModel().setStartTime(startTime); node.getModel().setEndTime(endTime); } private Date getEarliestStartTime(List<Scenario> scenarios) { List<Long> times = new ArrayList<>(); scenarios.stream().map(Scenario::getStartTimestamp).filter(time -> time.length() > 0).forEach(time -> times.add(Date.from(Instant.parse(time)).getTime())); return new Date(times.stream().reduce((start, end) -> start).get()); } private Date getStartTime(String timestamp) { return Date.from(Instant.parse(timestamp)); } private Date getEndTime(long startTime, List<Scenario> scenarios) { long endTime = 0; for (Scenario scenario : scenarios) { endTime = endTime + calculateDuration.apply(scenario.getBefore().stream().map(Hook::getResult).collect(Collectors.toList())); endTime = endTime + calculateDuration.apply(scenario.getAfter().stream().map(Hook::getResult).collect(Collectors.toList())); endTime = endTime + calculateDuration.apply(scenario.getSteps().stream().map(Step::getResult).collect(Collectors.toList())); endTime = endTime + calculateDuration.apply(scenario.getSteps().stream().flatMap(s -> s.getBefore().stream()).map(Hook::getResult).collect(Collectors.toList())); endTime = endTime + calculateDuration.apply(scenario.getSteps().stream().flatMap(s -> s.getAfter().stream()).map(Hook::getResult).collect(Collectors.toList())); } return new Date(startTime + endTime); } private Function<List<Result>, Long> calculateDuration = (source) -> source.stream().mapToLong(Result::getDuration).sum(); }