diff --git a/allure-selenide/src/main/java/io/qameta/allure/selenide/AllureSelenide.java b/allure-selenide/src/main/java/io/qameta/allure/selenide/AllureSelenide.java index ef45f1a78..bfa5efd07 100644 --- a/allure-selenide/src/main/java/io/qameta/allure/selenide/AllureSelenide.java +++ b/allure-selenide/src/main/java/io/qameta/allure/selenide/AllureSelenide.java @@ -32,9 +32,14 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.util.Collections; import java.util.EnumMap; +import java.util.IdentityHashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.WeakHashMap; import java.util.logging.Level; import static io.qameta.allure.util.ResultsUtils.getStatus; @@ -55,6 +60,7 @@ public class AllureSelenide implements LogEventListener { private boolean savePageHtml = true; private boolean includeSelenideLocatorsSteps = true; private final Map logTypesToSave = new EnumMap<>(LogType.class); + private final Set capturedErrors = Collections.newSetFromMap(new WeakHashMap<>()); private final AllureLifecycle lifecycle; /** @@ -167,52 +173,71 @@ public void beforeEvent(final LogEvent event) { } } + private boolean shouldCaptureAttachments(final Throwable error) { + if (Objects.isNull(error)) { + return true; + } + final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + for (Throwable current = error; current != null && visited.add(current); current = current.getCause()) { + if (capturedErrors.contains(current)) { + return false; + } + } + capturedErrors.add(error); + return true; + } + + private void captureAttachmentsOnFailure(final LogEvent event) { + if (!event.getStatus().equals(LogEvent.EventStatus.FAIL) || !shouldCaptureAttachments(event.getError())) { + return; + } + lifecycle.getCurrentExecutableKey().ifPresent(owner -> { + if (saveScreenshots) { + getScreenshotBytes() + .ifPresent( + bytes -> lifecycle.addAttachment( + owner, + "Screenshot", + "image/png", + new ByteArrayInputStream(bytes), + AttachmentOptions.empty() + ) + ); + } + if (savePageHtml) { + getPageSourceBytes() + .ifPresent( + bytes -> lifecycle.addAttachment( + owner, + "Page source", + "text/html", + new ByteArrayInputStream(bytes), + AttachmentOptions.empty() + ) + ); + } + if (!logTypesToSave.isEmpty()) { + logTypesToSave + .forEach((logType, level) -> { + final byte[] content = getBrowserLogs(logType, level).getBytes(UTF_8); + lifecycle.addAttachment( + owner, + "Logs from: " + logType, + "application/json", + new ByteArrayInputStream(content), + AttachmentOptions.withFileExtension(".txt") + ); + }); + } + }); + } + /** * {@inheritDoc} */ @Override public void afterEvent(final LogEvent event) { - if (event.getStatus().equals(LogEvent.EventStatus.FAIL)) { - lifecycle.getCurrentExecutableKey().ifPresent(owner -> { - if (saveScreenshots) { - getScreenshotBytes() - .ifPresent( - bytes -> lifecycle.addAttachment( - owner, - "Screenshot", - "image/png", - new ByteArrayInputStream(bytes), - AttachmentOptions.empty() - ) - ); - } - if (savePageHtml) { - getPageSourceBytes() - .ifPresent( - bytes -> lifecycle.addAttachment( - owner, - "Page source", - "text/html", - new ByteArrayInputStream(bytes), - AttachmentOptions.empty() - ) - ); - } - if (!logTypesToSave.isEmpty()) { - logTypesToSave - .forEach((logType, level) -> { - final byte[] content = getBrowserLogs(logType, level).getBytes(UTF_8); - lifecycle.addAttachment( - owner, - "Logs from: " + logType, - "application/json", - new ByteArrayInputStream(content), - AttachmentOptions.withFileExtension(".txt") - ); - }); - } - }); - } + captureAttachmentsOnFailure(event); if (stepsShouldBeLogged(event)) { lifecycle.getCurrentExecutableKey().ifPresent(owner -> { diff --git a/allure-selenide/src/test/java/io/qameta/allure/selenide/AllureSelenideTest.java b/allure-selenide/src/test/java/io/qameta/allure/selenide/AllureSelenideTest.java index 69cfbf82c..3993ef80b 100644 --- a/allure-selenide/src/test/java/io/qameta/allure/selenide/AllureSelenideTest.java +++ b/allure-selenide/src/test/java/io/qameta/allure/selenide/AllureSelenideTest.java @@ -396,6 +396,103 @@ void shouldSupportNestedSteps() { .containsExactly("child1", "child2", "child3"); } + @AllureFeatures.Attachments + @Test + void shouldDeduplicateAttachmentsOnNestedStepFailures() { + final WebDriver wdMock = mock(WebDriver.class, withSettings().extraInterfaces(TakesScreenshot.class)); + WebDriverRunner.setWebDriver(wdMock); + doReturn("screenshot-data".getBytes(StandardCharsets.UTF_8)) + .when((TakesScreenshot) wdMock).getScreenshotAs(OutputType.BYTES); + + final Throwable failure = new AssertionError("element not found"); + + final AllureResults results = runSelenideTestContext(() -> { + final AllureSelenide selenide = new AllureSelenide() + .savePageSource(false) + .screenshots(true); + SelenideLogger.addListener(UUID.randomUUID().toString(), selenide); + + final SelenideLog outerLog = SelenideLogger.beginStep("outer", "outerMethod()"); + final SelenideLog innerLog = SelenideLogger.beginStep("inner", "innerMethod()"); + + SelenideLogger.commitStep(innerLog, failure); + SelenideLogger.commitStep(outerLog, failure); + }); + + final List steps = extractAllStepsFromResults(results); + assertThat(steps).hasSize(1); + final StepResult outerStep = steps.get(0); + assertThat(outerStep.getSteps()).hasSize(1); + final StepResult innerStep = outerStep.getSteps().get(0); + + assertThat(innerStep.getAttachments()).hasSize(1); + assertThat(outerStep.getAttachments()).isEmpty(); + assertThat(results.getAttachments()).hasSize(1); + } + + @AllureFeatures.Attachments + @Test + void shouldDeduplicateAttachmentsWhenExceptionIsWrappedInCause() { + final WebDriver wdMock = mock(WebDriver.class, withSettings().extraInterfaces(TakesScreenshot.class)); + WebDriverRunner.setWebDriver(wdMock); + doReturn("screenshot-data".getBytes(StandardCharsets.UTF_8)) + .when((TakesScreenshot) wdMock).getScreenshotAs(OutputType.BYTES); + + final Throwable rootCause = new AssertionError("element not found"); + final Throwable wrappedError = new RuntimeException("step failed", rootCause); + + final AllureResults results = runSelenideTestContext(() -> { + final AllureSelenide selenide = new AllureSelenide() + .savePageSource(false) + .screenshots(true); + SelenideLogger.addListener(UUID.randomUUID().toString(), selenide); + + final SelenideLog outerLog = SelenideLogger.beginStep("outer", "outerMethod()"); + final SelenideLog innerLog = SelenideLogger.beginStep("inner", "innerMethod()"); + + SelenideLogger.commitStep(innerLog, rootCause); + SelenideLogger.commitStep(outerLog, wrappedError); + }); + + final List steps = extractAllStepsFromResults(results); + assertThat(steps).hasSize(1); + final StepResult outerStep = steps.get(0); + assertThat(outerStep.getSteps()).hasSize(1); + final StepResult innerStep = outerStep.getSteps().get(0); + + assertThat(innerStep.getAttachments()).hasSize(1); + assertThat(outerStep.getAttachments()).isEmpty(); + assertThat(results.getAttachments()).hasSize(1); + } + + @AllureFeatures.Attachments + @Test + void shouldAttachSeparatelyForDistinctFailures() { + final WebDriver wdMock = mock(WebDriver.class, withSettings().extraInterfaces(TakesScreenshot.class)); + WebDriverRunner.setWebDriver(wdMock); + doReturn("screenshot-data".getBytes(StandardCharsets.UTF_8)) + .when((TakesScreenshot) wdMock).getScreenshotAs(OutputType.BYTES); + + final AllureResults results = runSelenideTestContext(() -> { + final AllureSelenide selenide = new AllureSelenide() + .savePageSource(false) + .screenshots(true); + SelenideLogger.addListener(UUID.randomUUID().toString(), selenide); + + final SelenideLog step1 = SelenideLogger.beginStep("step1", "method1()"); + SelenideLogger.commitStep(step1, new AssertionError("first failure")); + + final SelenideLog step2 = SelenideLogger.beginStep("step2", "method2()"); + SelenideLogger.commitStep(step2, new AssertionError("second failure")); + }); + + final List steps = extractAllStepsFromResults(results); + assertThat(steps).hasSize(2); + assertThat(steps.get(0).getAttachments()).hasSize(1); + assertThat(steps.get(1).getAttachments()).hasSize(1); + assertThat(results.getAttachments()).hasSize(2); + } + private static StepResult extractStepFromResults(AllureResults results) { return results .getTestResults().iterator().next()