diff --git a/allure-assertj/build.gradle.kts b/allure-assertj/build.gradle.kts index e38cca0ea..7a31da16d 100644 --- a/allure-assertj/build.gradle.kts +++ b/allure-assertj/build.gradle.kts @@ -10,6 +10,8 @@ dependencies { testImplementation(project(":allure-java-commons-test")) testImplementation(project(":allure-junit-platform")) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.vintage:junit-vintage-engine") + testImplementation("org.junit.jupiter:junit-jupiter-params") } tasks.jar { diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java index fbf10f7f0..8105e630f 100644 --- a/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java @@ -28,6 +28,7 @@ import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; +import org.assertj.core.api.DefaultAssertionErrorCollector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +38,7 @@ import static io.qameta.allure.util.ResultsUtils.getStatus; import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import static io.qameta.allure.util.StepsUtils.getStepStatus; /** * @author charlie (Dmitry Baev). @@ -65,11 +67,35 @@ public void proxyMethod() { //pointcut body, should be empty } - @Pointcut("execution(public * org.assertj.core.api.AbstractAssert+.*(..)) && !proxyMethod()") + @Pointcut("execution(* org.assertj.core.api.*ByteBuddy*.*(..))") + public void generatedByteCode() { + //pointcut body, should be empty + } + + @Pointcut("execution(public * org.assertj.core.api.AbstractAssert+.*(..)) " + + "&& !proxyMethod() && !generatedByteCode()") public void anyAssert() { //pointcut body, should be empty } + @Pointcut("execution(public org.assertj.core.api.DefaultAssertionErrorCollector.new())") + public void softAssertCreation() { + //pointcut body, should be empty + } + + @After("softAssertCreation()") + public void setStepHookOnCollectedError(final JoinPoint joinPoint) { + final DefaultAssertionErrorCollector errorCollector = (DefaultAssertionErrorCollector) joinPoint.getTarget(); + errorCollector.setAfterAssertionErrorCollected(error -> { + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(step -> { + step.setStatus(getStatus(error).orElse(Status.BROKEN)) + .setStatusDetails(getStatusDetails(error).orElse(null)); + }); + } + }); + } + @After("anyAssertCreation()") public void logAssertCreation(final JoinPoint joinPoint) { final String actual = joinPoint.getArgs().length > 0 @@ -107,12 +133,24 @@ public void stepFailed(final Throwable e) { .setStatus(getStatus(e).orElse(Status.BROKEN)) .setStatusDetails(getStatusDetails(e).orElse(null))); getLifecycle().stopStep(); + // Outer method can catch exception so outer step status should be also changed (soft assertions case). + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(s -> s.setStatus(getStatus(e).orElse(Status.BROKEN))); + } } @AfterReturning(pointcut = "anyAssert()") public void stepStop() { - getLifecycle().updateStep(s -> s.setStatus(Status.PASSED)); - getLifecycle().stopStep(); + final Status currentStepStatus = getStepStatus(); + if (currentStepStatus == null) { + getLifecycle().updateStep(s -> s.setStatus(Status.PASSED)); + getLifecycle().stopStep(); + } else { + getLifecycle().stopStep(); + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(outerStep -> outerStep.setStatus(currentStepStatus)); + } + } } /** diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsBasicApproachesTest.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsBasicApproachesTest.java new file mode 100644 index 000000000..3cd977bb5 --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsBasicApproachesTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj; + +import io.qameta.allure.test.AllureFeatures; +import org.assertj.core.api.AutoCloseableSoftAssertions; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.StandardSoftAssertionsProvider; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Consumer; + +import static io.qameta.allure.assertj.util.SoftAssertionsUtils.eraseCollectedErrors; + +/** + * @author Achitheus (Yury Yurchenko). + */ +public class SoftAssertionsBasicApproachesTest { + + @AllureFeatures.Steps + @DisplayName("Check hardcoded soft assertions object") + @ParameterizedTest(name = "[{index}]: {arguments}") + @MethodSource(value = "io.qameta.allure.assertj.util.SoftAssertionsTestsProvider#softAssertionsTests") + void tests(String testName, Consumer test) { + SoftAssertions softly = new SoftAssertions(); + test.accept(softly); + } + + @AllureFeatures.Steps + @DisplayName("Check autocloseable soft assertions") + @ParameterizedTest(name = "[{index}]: {arguments}") + @MethodSource(value = "io.qameta.allure.assertj.util.SoftAssertionsTestsProvider#softAssertionsTests") + void autocloseableTests(String testName, Consumer test) { + try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + test.accept(softly); + eraseCollectedErrors(softly); + } + } + + @AllureFeatures.Steps + @DisplayName("Check SoftAssertions.assertSoftly() method") + @ParameterizedTest(name = "[{index}]: {arguments}") + @MethodSource(value = "io.qameta.allure.assertj.util.SoftAssertionsTestsProvider#softAssertionsTests") + void assertSoftlyMethodTests(String testName, Consumer test) { + SoftAssertions.assertSoftly(softly -> { + test.accept(softly); + eraseCollectedErrors(softly); + }); + } +} + diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsJUnit4RuleTest.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsJUnit4RuleTest.java new file mode 100644 index 000000000..50af84662 --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsJUnit4RuleTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj; + +import io.qameta.allure.test.AllureFeatures; +import org.assertj.core.api.JUnitSoftAssertions; +import org.assertj.core.api.StandardSoftAssertionsProvider; +import org.junit.Rule; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Consumer; + +import static io.qameta.allure.assertj.util.SoftAssertionsUtils.eraseCollectedErrors; + +/** + * @author Achitheus (Yury Yurchenko). + */ +public class SoftAssertionsJUnit4RuleTest { + + @Rule + public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); + + @AllureFeatures.Steps + @DisplayName("Check JUnit4 Rule soft assertions") + @ParameterizedTest(name = "[{index}]: {arguments}") + @MethodSource(value = "io.qameta.allure.assertj.util.SoftAssertionsTestsProvider#softAssertionsTests") + public void tests(String testName, Consumer test) { + test.accept(softly); + eraseCollectedErrors(softly); + } +} diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsJUnit5ExtensionTest.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsJUnit5ExtensionTest.java new file mode 100644 index 000000000..77c9a95af --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/SoftAssertionsJUnit5ExtensionTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj; + +import io.qameta.allure.test.AllureFeatures; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.StandardSoftAssertionsProvider; +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Consumer; + +import static io.qameta.allure.assertj.util.SoftAssertionsUtils.eraseCollectedErrors; + +/** + * @author Achitheus (Yury Yurchenko). + */ +@ExtendWith(SoftAssertionsExtension.class) +public class SoftAssertionsJUnit5ExtensionTest { + + @InjectSoftAssertions + private SoftAssertions softly; + + @AllureFeatures.Steps + @DisplayName("Check soft assertions obj injected as field by JUnit5 extension") + @ParameterizedTest(name = "[{index}]: {arguments}") + @MethodSource(value = "io.qameta.allure.assertj.util.SoftAssertionsTestsProvider#softAssertionsTests") + void fieldInjectionTests(String testName, Consumer test) { + test.accept(softly); + eraseCollectedErrors(softly); + } + + @AllureFeatures.Steps + @DisplayName("Check soft assertions obj injected as parameter by JUnit5 extension") + @ParameterizedTest(name = "[{index}]: {arguments}") + @MethodSource(value = "io.qameta.allure.assertj.util.SoftAssertionsTestsProvider#softAssertionsTests") + void parameterInjectionTests(String testName, Consumer test, SoftAssertions softly) { + test.accept(softly); + eraseCollectedErrors(softly); + } +} diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/util/ReflectionUtils.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/ReflectionUtils.java new file mode 100644 index 000000000..94c3d9141 --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/ReflectionUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj.util; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +public class ReflectionUtils { + + public static Consumer staticMethodAsConsumer(Method m) { + return param -> { + try { + m.invoke(null, param); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }; + } +} diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/util/SoftAssertionsTestsProvider.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/SoftAssertionsTestsProvider.java new file mode 100644 index 000000000..69e567cfe --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/SoftAssertionsTestsProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj.util; + +import io.qameta.allure.Allure; +import io.qameta.allure.Step; +import io.qameta.allure.aspects.StepsAspects; +import io.qameta.allure.assertj.AllureAspectJ; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.AllureResults; +import junit.framework.AssertionFailedError; +import org.assertj.core.api.StandardSoftAssertionsProvider; +import org.junit.jupiter.params.provider.Arguments; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static io.qameta.allure.Allure.step; +import static io.qameta.allure.assertj.util.ReflectionUtils.staticMethodAsConsumer; +import static io.qameta.allure.assertj.util.StringsUtils.humanReadableMethodOrClassName; +import static io.qameta.allure.model.Status.FAILED; +import static io.qameta.allure.model.Status.PASSED; +import static io.qameta.allure.test.RunUtils.runWithinTestContext; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Achitheus (Yury Yurchenko). + */ +@SuppressWarnings("unused") +public class SoftAssertionsTestsProvider { + public static final Logger LOGGER = LoggerFactory.getLogger(SoftAssertionsTestsProvider.class); + + public static Stream softAssertionsTests() { + return Arrays.stream(SoftAssertionsTestsProvider.class.getDeclaredMethods()) + .filter(method -> { + Class[] paramTypeArray = method.getParameterTypes(); + return !method.getName().contains("$") + && paramTypeArray.length == 1 + && paramTypeArray[0] == StandardSoftAssertionsProvider.class; + }) + .peek(method -> method.setAccessible(true)) + .map(method -> Arguments.of(humanReadableMethodOrClassName(method.getName()), staticMethodAsConsumer(method))); + } + + private static void afterAssertionErrorCollectedCallbackShouldWork(StandardSoftAssertionsProvider softly) { + final AllureResults results = runWithinTestContext(() -> { + step("Allure.step()", () -> { + annotationStep(() -> {}); + annotationStep(() -> {}); + annotationStep(() -> { + step("Allure.step()"); + step("Allure.step()", () -> { + annotationStep(() -> {}); + annotationStep(() -> softly.collectAssertionError(new AssertionFailedError("hellow"))); + }); + }); + annotationStep(() -> {}); + }); + }, AllureAspectJ::setLifecycle, Allure::setLifecycle, StepsAspects::setLifecycle); + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getStatus) + .containsExactly(FAILED); + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getStatus) + .containsExactly(PASSED, PASSED, FAILED, PASSED); + assertThat(softly.assertionErrorsCollected()).hasSize(1); + } + + private static void customMethodStepShouldBeFailed(StandardSoftAssertionsProvider softly) { + final AllureResults results = runWithinTestContext(() -> { + List notebookList = List.of("Tests string 0", "Tests string 1"); + step("Allure.step()", () -> { + softly.assertThatList(notebookList) + .hasSize(1_000_000) + .containsAnyOf("Passed", "Tests string 0"); + }); + step("Allure.step()", () -> { + softly.assertThatList(notebookList) + .hasSize(2) + .containsExactly("failed", "nope", "its failed"); + }); + }, AllureAspectJ::setLifecycle, Allure::setLifecycle); + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getStatus) + .containsExactly(FAILED, FAILED); + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getStatus) + .containsExactly(PASSED, FAILED, PASSED, + PASSED, PASSED, FAILED); + assertThat(softly.assertionErrorsCollected()).hasSize(2); + } + + private static void severalMiddleStepsShouldBeFailed(StandardSoftAssertionsProvider softly) { + final AllureResults results = runWithinTestContext(() -> { + softly.assertThat("Some test string") + .as("%s description passed", "awesome") + .containsAnyOf("passed", "blahblah", "me te") + .containsOnlyDigits() + .doesNotContainIgnoringCase("passed") + .startsWith("failed") + .containsPattern(".+ test .+") + .endsWith("failed"); + }, AllureAspectJ::setLifecycle); + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getStatus) + .containsExactly(PASSED, PASSED, PASSED, FAILED, PASSED, FAILED, PASSED, FAILED); + assertThat(softly.assertionErrorsCollected()).hasSize(3); + } + + @Step("@Step") + public static void annotationStep(Runnable runnable) { + runnable.run(); + } +} diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/util/SoftAssertionsUtils.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/SoftAssertionsUtils.java new file mode 100644 index 000000000..fcfbae6ed --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/SoftAssertionsUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj.util; + +import org.assertj.core.api.DefaultAssertionErrorCollector; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * @author Achitheus (Yury Yurchenko) + */ +@SuppressWarnings("unchecked") +public class SoftAssertionsUtils { + + public static void eraseCollectedErrors(DefaultAssertionErrorCollector softAssertions) { + try { + Field errorListField = DefaultAssertionErrorCollector.class.getDeclaredField("collectedAssertionErrors"); + errorListField.setAccessible(true); + List errorList = (List) errorListField.get(softAssertions.getDelegate().orElse(softAssertions)); + errorList.clear(); + } catch (ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/util/StringsUtils.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/StringsUtils.java new file mode 100644 index 000000000..1ac544076 --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/util/StringsUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.assertj.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class StringsUtils { + + /** + * Method converts camel-case string to human-readable one. + * It doesn't work correctly with names which contain abbreviations (DBConnection, helloASMFramework, etc.) + * + * @param camelCaseName standard Java style method or class name. + * @return human-readable name interpretation. + * @author Achitheus (Yury Yurchenko). + */ + public static String humanReadableMethodOrClassName(String camelCaseName) { + StringBuilder sb = new StringBuilder(); + Matcher matcher = Pattern.compile("([A-Za-z][^A-Z]*)").matcher(camelCaseName); + for (int i = 0; matcher.find(); i++) { + String group = matcher.group(1); + if (i == 0) { + sb.append(Character.toUpperCase(group.charAt(0))); + if (group.length() > 1) { + sb.append(group, 1, group.length()); + } + } else { + sb.append(" ").append(group.toLowerCase()); + } + } + return sb.toString(); + } +} diff --git a/allure-java-commons/src/main/java/io/qameta/allure/Allure.java b/allure-java-commons/src/main/java/io/qameta/allure/Allure.java index 21c90d2fa..767a2bac6 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/Allure.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/Allure.java @@ -39,6 +39,7 @@ import static io.qameta.allure.util.ResultsUtils.createParameter; import static io.qameta.allure.util.ResultsUtils.getStatus; import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import static io.qameta.allure.util.StepsUtils.getStepStatus; import static java.util.Arrays.asList; import static java.util.concurrent.CompletableFuture.supplyAsync; @@ -179,16 +180,29 @@ public static T step(final ThrowableContextRunnable runnable try { final T result = runnable.run(new DefaultStepContext(uuid)); - getLifecycle().updateStep(uuid, step -> step.setStatus(Status.PASSED)); + Status currentStepStatus = getStepStatus(); + if (currentStepStatus == null) { + getLifecycle().updateStep(uuid, step -> step.setStatus(Status.PASSED)); + getLifecycle().stopStep(); + } else { + getLifecycle().stopStep(); + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(outerStep -> outerStep.setStatus(currentStepStatus)); + } + } return result; } catch (Throwable throwable) { getLifecycle().updateStep(s -> s .setStatus(getStatus(throwable).orElse(Status.BROKEN)) .setStatusDetails(getStatusDetails(throwable).orElse(null))); + getLifecycle().stopStep(uuid); + // Is there possibility that anyone puts step(Throwable) calls into try/catch? + // Let the chain of steps be red even in such strange cases as this. + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(outerStep -> outerStep.setStatus(getStatus(throwable).orElse(Status.BROKEN))); + } ExceptionUtils.sneakyThrow(throwable); return null; - } finally { - getLifecycle().stopStep(uuid); } } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java index 5b9e0065f..13c667efd 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java @@ -299,6 +299,15 @@ public Optional getCurrentTestCaseOrStep() { return threadContext.getCurrent(); } + /** + * Returns uuid of current step. + * + * @return the uuid of current running test case or step. + */ + public Optional getCurrentStep() { + return threadContext.getCurrentStep(); + } + /** * Sets specified test case uuid as current. Note that * test case with such uuid should be created and existed in storage, otherwise diff --git a/allure-java-commons/src/main/java/io/qameta/allure/aspects/StepsAspects.java b/allure-java-commons/src/main/java/io/qameta/allure/aspects/StepsAspects.java index 407c82d40..affef0a0c 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/aspects/StepsAspects.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/aspects/StepsAspects.java @@ -36,6 +36,7 @@ import static io.qameta.allure.util.AspectUtils.getParameters; import static io.qameta.allure.util.ResultsUtils.getStatus; import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import static io.qameta.allure.util.StepsUtils.getStepStatus; /** * @author Dmitry Baev charlie@yandex-team.ru @@ -85,12 +86,25 @@ public void stepFailed(final Throwable e) { .setStatus(getStatus(e).orElse(Status.BROKEN)) .setStatusDetails(getStatusDetails(e).orElse(null))); getLifecycle().stopStep(); + // Is there possibility that anyone puts @Step-methods calls into try/catch? + // Let the chain of steps be red even in such strange cases as this. + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(outerStep -> outerStep.setStatus(getStatus(e).orElse(Status.BROKEN))); + } } @AfterReturning(pointcut = "anyMethod() && withStepAnnotation()") public void stepStop() { - getLifecycle().updateStep(s -> s.setStatus(Status.PASSED)); - getLifecycle().stopStep(); + Status currentStepStatus = getStepStatus(); + if (currentStepStatus == null) { + getLifecycle().updateStep(currentStep -> currentStep.setStatus(Status.PASSED)); + getLifecycle().stopStep(); + } else { + getLifecycle().stopStep(); + if (getLifecycle().getCurrentStep().isPresent()) { + getLifecycle().updateStep(outerStep -> outerStep.setStatus(currentStepStatus)); + } + } } /** diff --git a/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java b/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java index cc7236a79..ce7442c0c 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java @@ -28,6 +28,16 @@ public class AllureThreadContext { private final Context context = new Context(); + /** + * Returns last (most recent) uuid but not the root. + */ + public Optional getCurrentStep() { + final LinkedList uuids = context.get(); + return uuids.size() < 2 + ? Optional.empty() + : Optional.of(uuids.getFirst()); + } + /** * Returns last (most recent) uuid. */ diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/StepsUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/StepsUtils.java new file mode 100644 index 000000000..21a4535b8 --- /dev/null +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/StepsUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.util; + +import io.qameta.allure.model.Status; + +import java.util.concurrent.atomic.AtomicReference; + +import static io.qameta.allure.Allure.getLifecycle; + +public class StepsUtils { + + public static Status getStepStatus() { + AtomicReference outerStepRef = new AtomicReference<>(); + getLifecycle().updateStep(step -> outerStepRef.set(step.getStatus())); + return outerStepRef.get(); + } +}