|
| 1 | +package io.qameta.allure.cucumber3jvm; |
| 2 | + |
| 3 | +import cucumber.api.HookTestStep; |
| 4 | +import cucumber.api.HookType; |
| 5 | +import cucumber.api.PendingException; |
| 6 | +import cucumber.api.PickleStepTestStep; |
| 7 | +import cucumber.api.Result; |
| 8 | +import cucumber.api.TestCase; |
| 9 | +import cucumber.api.TestStep; |
| 10 | +import cucumber.api.event.EventHandler; |
| 11 | +import cucumber.api.event.EventPublisher; |
| 12 | +import cucumber.api.event.TestSourceRead; |
| 13 | +import cucumber.api.event.TestCaseStarted; |
| 14 | +import cucumber.api.event.TestCaseFinished; |
| 15 | +import cucumber.api.event.TestStepStarted; |
| 16 | +import cucumber.api.event.TestStepFinished; |
| 17 | +import cucumber.api.formatter.Formatter; |
| 18 | + |
| 19 | +import gherkin.ast.Feature; |
| 20 | +import gherkin.ast.ScenarioDefinition; |
| 21 | +import gherkin.ast.ScenarioOutline; |
| 22 | +import gherkin.ast.Examples; |
| 23 | +import gherkin.ast.TableRow; |
| 24 | +import gherkin.pickles.PickleCell; |
| 25 | +import gherkin.pickles.PickleRow; |
| 26 | +import gherkin.pickles.PickleTable; |
| 27 | +import gherkin.pickles.PickleTag; |
| 28 | +import io.qameta.allure.Allure; |
| 29 | +import io.qameta.allure.AllureLifecycle; |
| 30 | +import io.qameta.allure.model.Parameter; |
| 31 | +import io.qameta.allure.model.Status; |
| 32 | +import io.qameta.allure.model.TestResult; |
| 33 | +import io.qameta.allure.model.StepResult; |
| 34 | +import io.qameta.allure.model.StatusDetails; |
| 35 | +import io.qameta.allure.util.ResultsUtils; |
| 36 | + |
| 37 | +import java.io.ByteArrayInputStream; |
| 38 | +import java.nio.charset.Charset; |
| 39 | +import java.util.Deque; |
| 40 | +import java.util.LinkedList; |
| 41 | +import java.util.List; |
| 42 | +import java.util.Map; |
| 43 | +import java.util.HashMap; |
| 44 | +import java.util.Optional; |
| 45 | +import java.util.UUID; |
| 46 | +import java.util.function.Consumer; |
| 47 | +import java.util.stream.Collectors; |
| 48 | +import java.util.stream.IntStream; |
| 49 | + |
| 50 | +/** |
| 51 | + * Allure plugin for Cucumber JVM 3.0. |
| 52 | + */ |
| 53 | +@SuppressWarnings({ |
| 54 | + "PMD.ExcessiveImports", |
| 55 | + "ClassFanOutComplexity", "ClassDataAbstractionCoupling" |
| 56 | +}) |
| 57 | +public class AllureCucumber3Jvm implements Formatter { |
| 58 | + |
| 59 | + private final AllureLifecycle lifecycle; |
| 60 | + |
| 61 | + private final Map<String, String> scenarioUuids = new HashMap<>(); |
| 62 | + |
| 63 | + private final CucumberSourceUtils cucumberSourceUtils = new CucumberSourceUtils(); |
| 64 | + private Feature currentFeature; |
| 65 | + private String currentFeatureFile; |
| 66 | + private TestCase currentTestCase; |
| 67 | + |
| 68 | + private final EventHandler<TestSourceRead> featureStartedHandler = this::handleFeatureStartedHandler; |
| 69 | + private final EventHandler<TestCaseStarted> caseStartedHandler = this::handleTestCaseStarted; |
| 70 | + private final EventHandler<TestCaseFinished> caseFinishedHandler = this::handleTestCaseFinished; |
| 71 | + private final EventHandler<TestStepStarted> stepStartedHandler = this::handleTestStepStarted; |
| 72 | + private final EventHandler<TestStepFinished> stepFinishedHandler = this::handleTestStepFinished; |
| 73 | + |
| 74 | + public AllureCucumber3Jvm() { |
| 75 | + this.lifecycle = Allure.getLifecycle(); |
| 76 | + } |
| 77 | + |
| 78 | + @Override |
| 79 | + public void setEventPublisher(final EventPublisher publisher) { |
| 80 | + publisher.registerHandlerFor(TestSourceRead.class, featureStartedHandler); |
| 81 | + |
| 82 | + publisher.registerHandlerFor(TestCaseStarted.class, caseStartedHandler); |
| 83 | + publisher.registerHandlerFor(TestCaseFinished.class, caseFinishedHandler); |
| 84 | + |
| 85 | + publisher.registerHandlerFor(TestStepStarted.class, stepStartedHandler); |
| 86 | + publisher.registerHandlerFor(TestStepFinished.class, stepFinishedHandler); |
| 87 | + } |
| 88 | + |
| 89 | + /* |
| 90 | + Event Handlers |
| 91 | + */ |
| 92 | + |
| 93 | + private void handleFeatureStartedHandler(final TestSourceRead event) { |
| 94 | + cucumberSourceUtils.addTestSourceReadEvent(event.uri, event); |
| 95 | + } |
| 96 | + |
| 97 | + private void handleTestCaseStarted(final TestCaseStarted event) { |
| 98 | + currentFeatureFile = event.testCase.getUri(); |
| 99 | + currentFeature = cucumberSourceUtils.getFeature(currentFeatureFile); |
| 100 | + |
| 101 | + currentTestCase = event.testCase; |
| 102 | + |
| 103 | + final Deque<PickleTag> tags = new LinkedList<>(); |
| 104 | + tags.addAll(event.testCase.getTags()); |
| 105 | + |
| 106 | + final LabelBuilder labelBuilder = new LabelBuilder(currentFeature, event.testCase, tags); |
| 107 | + |
| 108 | + final TestResult result = new TestResult() |
| 109 | + .withUuid(getTestCaseUuid(event.testCase)) |
| 110 | + .withHistoryId(getHistoryId(event.testCase)) |
| 111 | + .withName(event.testCase.getName()) |
| 112 | + .withLabels(labelBuilder.getScenarioLabels()) |
| 113 | + .withLinks(labelBuilder.getScenarioLinks()); |
| 114 | + |
| 115 | + final ScenarioDefinition scenarioDefinition = |
| 116 | + cucumberSourceUtils.getScenarioDefinition(currentFeatureFile, currentTestCase.getLine()); |
| 117 | + if (scenarioDefinition instanceof ScenarioOutline) { |
| 118 | + result.withParameters( |
| 119 | + getExamplesAsParameters((ScenarioOutline) scenarioDefinition) |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + if (currentFeature.getDescription() != null && !currentFeature.getDescription().isEmpty()) { |
| 124 | + result.withDescription(currentFeature.getDescription()); |
| 125 | + } |
| 126 | + |
| 127 | + lifecycle.scheduleTestCase(result); |
| 128 | + lifecycle.startTestCase(getTestCaseUuid(event.testCase)); |
| 129 | + } |
| 130 | + |
| 131 | + private void handleTestCaseFinished(final TestCaseFinished event) { |
| 132 | + final StatusDetails statusDetails = |
| 133 | + ResultsUtils.getStatusDetails(event.result.getError()).orElse(new StatusDetails()); |
| 134 | + |
| 135 | + if (statusDetails.getMessage() != null && statusDetails.getTrace() != null) { |
| 136 | + lifecycle.updateTestCase(getTestCaseUuid(event.testCase), scenarioResult -> |
| 137 | + scenarioResult |
| 138 | + .withStatus(translateTestCaseStatus(event.result)) |
| 139 | + .withStatusDetails(statusDetails)); |
| 140 | + } else { |
| 141 | + lifecycle.updateTestCase(getTestCaseUuid(event.testCase), scenarioResult -> |
| 142 | + scenarioResult |
| 143 | + .withStatus(translateTestCaseStatus(event.result))); |
| 144 | + } |
| 145 | + |
| 146 | + lifecycle.stopTestCase(getTestCaseUuid(event.testCase)); |
| 147 | + lifecycle.writeTestCase(getTestCaseUuid(event.testCase)); |
| 148 | + } |
| 149 | + |
| 150 | + private void handleTestStepStarted(final TestStepStarted event) { |
| 151 | + if (event.testStep instanceof PickleStepTestStep) { |
| 152 | + final PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) event.testStep; |
| 153 | + |
| 154 | + final String stepKeyword = Optional.ofNullable( |
| 155 | + cucumberSourceUtils.getKeywordFromSource(currentFeatureFile, pickleStepTestStep.getStepLine()) |
| 156 | + ).orElse("UNDEFINED"); |
| 157 | + |
| 158 | + final StepResult stepResult = new StepResult() |
| 159 | + .withName(String.format("%s %s", stepKeyword, pickleStepTestStep.getPickleStep().getText())) |
| 160 | + .withStart(System.currentTimeMillis()); |
| 161 | + |
| 162 | + lifecycle.startStep(getTestCaseUuid(currentTestCase), getStepUuid(event.testStep), stepResult); |
| 163 | + |
| 164 | + pickleStepTestStep.getStepArgument().stream() |
| 165 | + .filter(argument -> argument instanceof PickleTable) |
| 166 | + .findFirst() |
| 167 | + .ifPresent(table -> createDataTableAttachment((PickleTable) table)); |
| 168 | + } else if (event.testStep instanceof HookTestStep) { |
| 169 | + final HookTestStep hookTestStep = (HookTestStep) event.testStep; |
| 170 | + final StepResult stepResult = new StepResult() |
| 171 | + .withName(hookTestStep.getHookType().toString()) |
| 172 | + .withStart(System.currentTimeMillis()); |
| 173 | + |
| 174 | + lifecycle.startStep(getTestCaseUuid(currentTestCase), getHookStepUuid(event.testStep), stepResult); |
| 175 | + } else { |
| 176 | + throw new IllegalStateException(); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + private void handleTestStepFinished(final TestStepFinished event) { |
| 181 | + if (event.testStep instanceof PickleStepTestStep) { |
| 182 | + handlePickleStep(event); |
| 183 | + } else if (event.testStep instanceof HookTestStep) { |
| 184 | + handleHookStep(event); |
| 185 | + } else { |
| 186 | + throw new IllegalStateException(); |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + /* |
| 191 | + Utility Methods |
| 192 | + */ |
| 193 | + |
| 194 | + private String getTestCaseUuid(final TestCase testCase) { |
| 195 | + return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); |
| 196 | + } |
| 197 | + |
| 198 | + private String getStepUuid(final TestStep step) { |
| 199 | + final PickleStepTestStep pickleStep = (PickleStepTestStep) step; |
| 200 | + return currentFeature.getName() + getTestCaseUuid(currentTestCase) |
| 201 | + + pickleStep.getStepText() + pickleStep.getStepLine(); |
| 202 | + } |
| 203 | + |
| 204 | + private String getHookStepUuid(final TestStep step) { |
| 205 | + final HookTestStep hookTestStep = (HookTestStep) step; |
| 206 | + return currentFeature.getName() + getTestCaseUuid(currentTestCase) |
| 207 | + + hookTestStep.getHookType().toString() + step.getCodeLocation(); |
| 208 | + } |
| 209 | + |
| 210 | + private String getHistoryId(final TestCase testCase) { |
| 211 | + final String testCaseLocation = testCase.getUri() + ":" + testCase.getLine(); |
| 212 | + return Utils.md5(testCaseLocation); |
| 213 | + } |
| 214 | + |
| 215 | + private Status translateTestCaseStatus(final Result testCaseResult) { |
| 216 | + Status allureStatus; |
| 217 | + if (testCaseResult.getStatus() == Result.Type.UNDEFINED || testCaseResult.getStatus() == Result.Type.PENDING) { |
| 218 | + allureStatus = Status.SKIPPED; |
| 219 | + } else { |
| 220 | + try { |
| 221 | + allureStatus = Status.fromValue(testCaseResult.getStatus().lowerCaseName()); |
| 222 | + } catch (IllegalArgumentException e) { |
| 223 | + allureStatus = Status.BROKEN; |
| 224 | + } |
| 225 | + } |
| 226 | + return allureStatus; |
| 227 | + } |
| 228 | + |
| 229 | + private List<Parameter> getExamplesAsParameters(final ScenarioOutline scenarioOutline) { |
| 230 | + final Examples examples = scenarioOutline.getExamples().get(0); |
| 231 | + final TableRow row = examples.getTableBody().stream() |
| 232 | + .filter(example -> example.getLocation().getLine() == currentTestCase.getLine()) |
| 233 | + .findFirst().get(); |
| 234 | + return IntStream.range(0, examples.getTableHeader().getCells().size()).mapToObj(index -> { |
| 235 | + final String name = examples.getTableHeader().getCells().get(index).getValue(); |
| 236 | + final String value = row.getCells().get(index).getValue(); |
| 237 | + return new Parameter().withName(name).withValue(value); |
| 238 | + }).collect(Collectors.toList()); |
| 239 | + } |
| 240 | + |
| 241 | + private void createDataTableAttachment(final PickleTable pickleTable) { |
| 242 | + final List<PickleRow> rows = pickleTable.getRows(); |
| 243 | + |
| 244 | + final StringBuilder dataTableCsv = new StringBuilder(); |
| 245 | + if (!rows.isEmpty()) { |
| 246 | + rows.forEach(dataTableRow -> { |
| 247 | + dataTableCsv.append( |
| 248 | + dataTableRow.getCells().stream() |
| 249 | + .map(PickleCell::getValue) |
| 250 | + .collect(Collectors.joining("\t")) |
| 251 | + ); |
| 252 | + dataTableCsv.append('\n'); |
| 253 | + }); |
| 254 | + |
| 255 | + final String attachmentSource = lifecycle |
| 256 | + .prepareAttachment("Data table", "text/tab-separated-values", "csv"); |
| 257 | + lifecycle.writeAttachment(attachmentSource, |
| 258 | + new ByteArrayInputStream(dataTableCsv.toString().getBytes(Charset.forName("UTF-8")))); |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + private void handleHookStep(final TestStepFinished event) { |
| 263 | + final String uuid = getHookStepUuid(event.testStep); |
| 264 | + Consumer<StepResult> stepResult = result -> result.withStatus(translateTestCaseStatus(event.result)); |
| 265 | + |
| 266 | + if (!Status.PASSED.equals(translateTestCaseStatus(event.result))) { |
| 267 | + final StatusDetails statusDetails = ResultsUtils.getStatusDetails(event.result.getError()).get(); |
| 268 | + final HookTestStep hookTestStep = (HookTestStep) event.testStep; |
| 269 | + if (hookTestStep.getHookType() == HookType.Before) { |
| 270 | + final TagParser tagParser = new TagParser(currentFeature, currentTestCase); |
| 271 | + statusDetails |
| 272 | + .withMessage("Before is failed: " + event.result.getError().getLocalizedMessage()) |
| 273 | + .withFlaky(tagParser.isFlaky()) |
| 274 | + .withMuted(tagParser.isMuted()) |
| 275 | + .withKnown(tagParser.isKnown()); |
| 276 | + lifecycle.updateTestCase(getTestCaseUuid(currentTestCase), scenarioResult -> |
| 277 | + scenarioResult.withStatus(Status.SKIPPED) |
| 278 | + .withStatusDetails(statusDetails)); |
| 279 | + } |
| 280 | + stepResult = result -> result |
| 281 | + .withStatus(translateTestCaseStatus(event.result)) |
| 282 | + .withStatusDetails(statusDetails); |
| 283 | + } |
| 284 | + |
| 285 | + lifecycle.updateStep(uuid, stepResult); |
| 286 | + lifecycle.stopStep(uuid); |
| 287 | + } |
| 288 | + |
| 289 | + private void handlePickleStep(final TestStepFinished event) { |
| 290 | + final StatusDetails statusDetails; |
| 291 | + if (event.result.getStatus() == Result.Type.UNDEFINED) { |
| 292 | + statusDetails = |
| 293 | + ResultsUtils.getStatusDetails(new PendingException("TODO: implement me")) |
| 294 | + .orElse(new StatusDetails()); |
| 295 | + lifecycle.updateTestCase(getTestCaseUuid(currentTestCase), scenarioResult -> |
| 296 | + scenarioResult |
| 297 | + .withStatus(translateTestCaseStatus(event.result)) |
| 298 | + .withStatusDetails(statusDetails)); |
| 299 | + } else { |
| 300 | + statusDetails = |
| 301 | + ResultsUtils.getStatusDetails(event.result.getError()) |
| 302 | + .orElse(new StatusDetails()); |
| 303 | + } |
| 304 | + |
| 305 | + final TagParser tagParser = new TagParser(currentFeature, currentTestCase); |
| 306 | + statusDetails |
| 307 | + .withFlaky(tagParser.isFlaky()) |
| 308 | + .withMuted(tagParser.isMuted()) |
| 309 | + .withKnown(tagParser.isKnown()); |
| 310 | + |
| 311 | + lifecycle.updateStep(getStepUuid(event.testStep), stepResult -> |
| 312 | + stepResult.withStatus(translateTestCaseStatus(event.result))); |
| 313 | + lifecycle.stopStep(getStepUuid(event.testStep)); |
| 314 | + } |
| 315 | +} |
0 commit comments