io.qameta.allure.junitplatform.AllureJunitPlatform Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of allure-junit-platform Show documentation
Show all versions of allure-junit-platform Show documentation
Module allure-junit-platform of Allure Framework.
The newest version!
/*
* Copyright 2016-2024 Qameta Software Inc
*
* 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.junitplatform;
import io.qameta.allure.Allure;
import io.qameta.allure.AllureLifecycle;
import io.qameta.allure.Description;
import io.qameta.allure.Severity;
import io.qameta.allure.SeverityLevel;
import io.qameta.allure.model.FixtureResult;
import io.qameta.allure.model.Label;
import io.qameta.allure.model.Parameter;
import io.qameta.allure.model.Stage;
import io.qameta.allure.model.Status;
import io.qameta.allure.model.StatusDetails;
import io.qameta.allure.model.TestResult;
import io.qameta.allure.model.TestResultContainer;
import io.qameta.allure.util.AnnotationUtils;
import io.qameta.allure.util.ResultsUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.TestTag;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static io.qameta.allure.model.Status.FAILED;
import static io.qameta.allure.model.Status.PASSED;
import static io.qameta.allure.model.Status.SKIPPED;
import static io.qameta.allure.util.ResultsUtils.ALLURE_ID_LABEL_NAME;
import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel;
import static io.qameta.allure.util.ResultsUtils.createHostLabel;
import static io.qameta.allure.util.ResultsUtils.createLanguageLabel;
import static io.qameta.allure.util.ResultsUtils.createPackageLabel;
import static io.qameta.allure.util.ResultsUtils.createSuiteLabel;
import static io.qameta.allure.util.ResultsUtils.createTestClassLabel;
import static io.qameta.allure.util.ResultsUtils.createTestMethodLabel;
import static io.qameta.allure.util.ResultsUtils.createThreadLabel;
import static io.qameta.allure.util.ResultsUtils.getMd5Digest;
import static io.qameta.allure.util.ResultsUtils.getProvidedLabels;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* @author ehborisov
*/
@SuppressWarnings({
"ClassDataAbstractionCoupling",
"ClassFanOutComplexity",
"MultipleStringLiterals",
"PMD.CyclomaticComplexity",
"PMD.NcssCount",
"PMD.TooManyMethods",
})
public class AllureJunitPlatform implements TestExecutionListener {
public static final String ALLURE_REPORT_ENTRY_BLANK_PREFIX
= "ALLURE_REPORT_ENTRY_BLANK_PREFIX__";
public static final String ALLURE_PARAMETER = "allure.parameter";
public static final String ALLURE_PARAMETER_VALUE_KEY = "value";
public static final String ALLURE_PARAMETER_MODE_KEY = "mode";
public static final String ALLURE_PARAMETER_EXCLUDED_KEY = "excluded";
public static final String ALLURE_FIXTURE = "allure.fixture";
public static final String PREPARE = "prepare";
public static final String TEAR_DOWN = "tear_down";
public static final String EVENT_START = "start";
public static final String EVENT_STOP = "stop";
public static final String EVENT_FAILURE = "failure";
public static final String JUNIT_PLATFORM_UNIQUE_ID = "junit.platform.uniqueid";
private static final Logger LOGGER = LoggerFactory.getLogger(AllureJunitPlatform.class);
private static final String STDOUT = "stdout";
private static final String STDERR = "stderr";
private static final String TEXT_PLAIN = "text/plain";
private static final String TXT_EXTENSION = ".txt";
private static final boolean HAS_SPOCK2_IN_CLASSPATH
= isClassAvailableOnClasspath("io.qameta.allure.spock2.AllureSpock2");
private static final boolean HAS_CUCUMBERJVM7_IN_CLASSPATH
= isClassAvailableOnClasspath("io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm");
private static final boolean HAS_CUCUMBERJVM6_IN_CLASSPATH
= isClassAvailableOnClasspath("io.qameta.allure.cucumber6jvm.AllureCucumber6Jvm");
private static final boolean HAS_CUCUMBERJVM5_IN_CLASSPATH
= isClassAvailableOnClasspath("io.qameta.allure.cucumber5jvm.AllureCucumber5Jvm");
private static final boolean HAS_CUCUMBERJVM4_IN_CLASSPATH
= isClassAvailableOnClasspath("io.qameta.allure.cucumber4jvm.AllureCucumber4Jvm");
private static final String ENGINE_SPOCK2 = "spock";
private static final String ENGINE_CUCUMBER = "cucumber";
private final ThreadLocal testPlanStorage = new InheritableThreadLocal<>();
private final ThreadLocal tests = new InheritableThreadLocal() {
@Override
protected Uuids initialValue() {
return new Uuids();
}
};
private final ThreadLocal containers = new InheritableThreadLocal() {
@Override
protected Uuids initialValue() {
return new Uuids();
}
};
private final AllureLifecycle lifecycle;
public AllureJunitPlatform(final AllureLifecycle lifecycle) {
this.lifecycle = lifecycle;
}
public AllureJunitPlatform() {
this.lifecycle = Allure.getLifecycle();
}
public AllureLifecycle getLifecycle() {
return lifecycle;
}
@SuppressWarnings({"CyclomaticComplexity", "BooleanExpressionComplexity"})
private boolean shouldSkipReportingFor(final TestIdentifier testIdentifier) {
// Always skip root
if (!testIdentifier.getParentId().isPresent()) {
return true;
}
final Optional maybeEngine = getEngine(testIdentifier);
// can't find the engine, don't know if it's possible but just in case
// keep reporting such nodes.
if (!maybeEngine.isPresent()) {
return false;
}
final String engine = maybeEngine.get();
return HAS_SPOCK2_IN_CLASSPATH && ENGINE_SPOCK2.equals(engine)
|| (HAS_CUCUMBERJVM7_IN_CLASSPATH
|| HAS_CUCUMBERJVM6_IN_CLASSPATH
|| HAS_CUCUMBERJVM5_IN_CLASSPATH
|| HAS_CUCUMBERJVM4_IN_CLASSPATH
) && ENGINE_CUCUMBER.equals(engine);
}
private Optional getEngine(final TestIdentifier testIdentifier) {
final UniqueId uniqueId = testIdentifier.getUniqueIdObject();
final List segments = uniqueId.getSegments();
// since junit-platform-suite engine creates nested engine segments
// we need to lookup for the last one with type engine
// to determinate the actual used engine:
// [engine:junit-platform-suite]/[suite:org.example.JUnitRunnerTest]/[engine:cucumber]/...
for (int i = segments.size() - 1; i >= 0; i--) {
final UniqueId.Segment segment = segments.get(i);
if ("engine".equals(segment.getType())) {
return Optional.of(segment.getValue());
}
}
return Optional.empty();
}
private static boolean isClassAvailableOnClasspath(final String clazz) {
try {
AllureJunitPlatform.class.getClassLoader().loadClass(clazz);
return true;
} catch (Exception ignored) {
return false;
}
}
@Override
public void testPlanExecutionStarted(final TestPlan testPlan) {
testPlanStorage.set(testPlan);
tests.set(new Uuids());
containers.set(new Uuids());
}
@Override
public void testPlanExecutionFinished(final TestPlan testPlan) {
testPlanStorage.remove();
tests.remove();
containers.remove();
}
@Override
public void executionStarted(final TestIdentifier testIdentifier) {
if (shouldSkipReportingFor(testIdentifier)) {
return;
}
// create container for every TestIdentifier. We need containers for tests in order
// to support method fixtures.
startTestContainer(testIdentifier);
if (testIdentifier.isTest()) {
startTestCase(testIdentifier);
}
}
@Override
public void executionFinished(final TestIdentifier testIdentifier,
final TestExecutionResult testExecutionResult) {
if (shouldSkipReportingFor(testIdentifier)) {
return;
}
final Status status = extractStatus(testExecutionResult);
final StatusDetails statusDetails = testExecutionResult.getThrowable()
.flatMap(ResultsUtils::getStatusDetails)
.orElse(null);
if (testIdentifier.isTest()) {
stopTestCase(testIdentifier, status, statusDetails);
} else if (testExecutionResult.getStatus() != TestExecutionResult.Status.SUCCESSFUL) {
// report failed containers as fake test results
startTestCase(testIdentifier);
stopTestCase(testIdentifier, status, statusDetails);
}
stopTestContainer(testIdentifier);
}
@Override
public void executionSkipped(final TestIdentifier testIdentifier,
final String reason) {
if (shouldSkipReportingFor(testIdentifier)) {
return;
}
final TestPlan testPlan = testPlanStorage.get();
if (Objects.isNull(testPlan)) {
return;
}
reportNested(
testPlan,
testIdentifier,
SKIPPED,
new StatusDetails().setMessage(reason),
new HashSet<>()
);
}
@SuppressWarnings({"ReturnCount", "PMD.NcssCount", "CyclomaticComplexity"})
@Override
public void reportingEntryPublished(final TestIdentifier testIdentifier,
final ReportEntry entry) {
if (shouldSkipReportingFor(testIdentifier)) {
return;
}
final Map keyValuePairs = unwrap(entry.getKeyValuePairs());
if (keyValuePairs.containsKey(ALLURE_FIXTURE)) {
processFixtureEvent(testIdentifier, keyValuePairs);
return;
}
if (keyValuePairs.containsKey(ALLURE_PARAMETER)) {
processParameterEvent(keyValuePairs);
return;
}
if (keyValuePairs.containsKey(STDOUT)) {
final String content = keyValuePairs.getOrDefault(STDOUT, "");
getLifecycle().addAttachment("Stdout", TEXT_PLAIN, TXT_EXTENSION, content.getBytes(UTF_8));
}
if (keyValuePairs.containsKey(STDERR)) {
final String content = keyValuePairs.getOrDefault(STDERR, "");
getLifecycle().addAttachment("Stderr", TEXT_PLAIN, TXT_EXTENSION, content.getBytes(UTF_8));
}
}
@SuppressWarnings("PMD.InefficientEmptyStringCheck")
private Map unwrap(final Map data) {
final Map res = new HashMap<>();
data.forEach((key, value) -> {
if (Objects.nonNull(value)
&& value.trim().isEmpty()
&& value.startsWith(ALLURE_REPORT_ENTRY_BLANK_PREFIX)) {
res.put(key, value.substring(ALLURE_REPORT_ENTRY_BLANK_PREFIX.length()));
} else {
res.put(key, value);
}
}
);
return res;
}
private void processParameterEvent(final Map keyValuePairs) {
final String name = keyValuePairs.get(ALLURE_PARAMETER);
final String value = keyValuePairs.get(ALLURE_PARAMETER_VALUE_KEY);
final Parameter parameter;
if (Objects.nonNull(value) && value.startsWith(ALLURE_REPORT_ENTRY_BLANK_PREFIX)) {
parameter = ResultsUtils.createParameter(
name,
value.substring(ALLURE_REPORT_ENTRY_BLANK_PREFIX.length())
);
} else {
parameter = ResultsUtils.createParameter(name, value);
}
if (keyValuePairs.containsKey(ALLURE_PARAMETER_MODE_KEY)) {
final String modeString = keyValuePairs.get(ALLURE_PARAMETER_MODE_KEY);
Stream.of(Parameter.Mode.values())
.filter(mode -> mode.name().equalsIgnoreCase(modeString))
.findAny()
.ifPresent(parameter::setMode);
}
if (keyValuePairs.containsKey(ALLURE_PARAMETER_EXCLUDED_KEY)) {
final String excludedString = keyValuePairs.get(ALLURE_PARAMETER_EXCLUDED_KEY);
Optional.ofNullable(excludedString)
.map(Boolean::parseBoolean)
.ifPresent(parameter::setExcluded);
}
getLifecycle().updateTestCase(tr -> tr.getParameters()
.add(parameter)
);
}
@SuppressWarnings({"ReturnCount"})
private void processFixtureEvent(final TestIdentifier testIdentifier,
final Map keyValuePairs) {
final String type = keyValuePairs.get(ALLURE_FIXTURE);
final String event = keyValuePairs.get("event");
// skip for invalid events
if (Objects.isNull(type) || Objects.isNull(event)) {
return;
}
switch (event) {
case EVENT_START:
final Optional maybeParent = getContainer(testIdentifier);
if (!maybeParent.isPresent()) {
return;
}
final String parentUuid = maybeParent.get();
startFixture(parentUuid, type, keyValuePairs);
return;
case EVENT_FAILURE:
failFixture(keyValuePairs);
resetContext(testIdentifier);
return;
case EVENT_STOP:
stopFixture(keyValuePairs);
resetContext(testIdentifier);
return;
default:
break;
}
}
private void resetContext(final TestIdentifier testIdentifier) {
// in case of fixtures that reported within a test we need to return current
// test case uuid to allure thread local storage
Optional.of(testIdentifier)
.filter(TestIdentifier::isTest)
.flatMap(this::getTest)
.ifPresent(Allure.getLifecycle()::setCurrentTestCase);
}
private void reportNested(final TestPlan testPlan,
final TestIdentifier testIdentifier,
final Status status,
final StatusDetails statusDetails,
final Set visited) {
final Set children = testPlan.getChildren(testIdentifier);
if (testIdentifier.isTest() || children.isEmpty()) {
startTestCase(testIdentifier);
stopTestCase(testIdentifier, status, statusDetails);
}
visited.add(testIdentifier);
children.stream()
.filter(id -> !visited.contains(id))
.forEach(child -> reportNested(testPlan, child, status, statusDetails, visited));
}
protected Status getStatus(final Throwable throwable) {
return ResultsUtils.getStatus(throwable).orElse(FAILED);
}
private void startTestContainer(final TestIdentifier testIdentifier) {
final String uuid = getOrCreateContainer(testIdentifier);
final TestResultContainer result = new TestResultContainer()
.setUuid(uuid)
.setName(testIdentifier.getDisplayName());
getLifecycle().startTestContainer(result);
}
private void stopTestContainer(final TestIdentifier testIdentifier) {
final Optional maybeUuid = getContainer(testIdentifier);
if (!maybeUuid.isPresent()) {
return;
}
final String uuid = maybeUuid.get();
final TestPlan context = testPlanStorage.get();
final List children = Optional.ofNullable(context)
.map(tp -> tp.getDescendants(testIdentifier))
.orElseGet(Collections::emptySet)
.stream()
.filter(TestIdentifier::isTest)
.map(this::getTest)
.filter(Optional::isPresent)
.map(Optional::get)
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
getTest(testIdentifier).ifPresent(children::add);
getLifecycle().updateTestContainer(uuid, container -> container.setChildren(children));
getLifecycle().stopTestContainer(uuid);
getLifecycle().writeTestContainer(uuid);
}
private void startFixture(final String parentUuid,
final String type,
final Map keyValue) {
final String uuid = keyValue.get("uuid");
if (Objects.isNull(uuid)) {
return;
}
final String name = keyValue.getOrDefault("name", "Unknown");
final FixtureResult result = new FixtureResult().setName(name);
switch (type) {
case PREPARE:
getLifecycle().startPrepareFixture(parentUuid, uuid, result);
return;
case TEAR_DOWN:
getLifecycle().startTearDownFixture(parentUuid, uuid, result);
return;
default:
LOGGER.debug("unknown fixture type {}", type);
break;
}
}
private void failFixture(final Map keyValue) {
final String uuid = keyValue.get("uuid");
if (Objects.isNull(uuid)) {
return;
}
getLifecycle().updateFixture(uuid, fixtureResult -> {
Optional.of(keyValue.get("status"))
.map(Status::fromValue)
.ifPresent(fixtureResult::setStatus);
fixtureResult.setStatusDetails(new StatusDetails());
Optional.of(keyValue.get("message"))
.ifPresent(fixtureResult.getStatusDetails()::setMessage);
Optional.of(keyValue.get("trace"))
.ifPresent(fixtureResult.getStatusDetails()::setTrace);
});
getLifecycle().stopFixture(uuid);
}
private void stopFixture(final Map keyValue) {
final String uuid = keyValue.get("uuid");
if (Objects.isNull(uuid)) {
return;
}
getLifecycle().updateFixture(uuid, fixtureResult -> fixtureResult.setStatus(PASSED));
getLifecycle().stopFixture(uuid);
}
@SuppressWarnings("PMD.NcssCount")
private void startTestCase(final TestIdentifier testIdentifier) {
final String uuid = getOrCreateTest(testIdentifier);
final Optional testSource = testIdentifier.getSource();
final Optional testMethod = testSource
.flatMap(AllureJunitPlatformUtils::getTestMethod);
final Optional> testClass = testSource
.flatMap(AllureJunitPlatformUtils::getTestClass);
final boolean testTemplate = "test-template-invocation"
.equals(testIdentifier.getUniqueIdObject().getLastSegment().getType());
final Optional maybeParent = Optional.of(testPlanStorage)
.map(ThreadLocal::get)
.flatMap(tp -> tp.getParent(testIdentifier));
final TestResult result = new TestResult()
.setUuid(uuid)
.setName(testTemplate && maybeParent.isPresent()
? maybeParent.get().getDisplayName() + " " + testIdentifier.getDisplayName()
: testIdentifier.getDisplayName()
)
.setLabels(getTags(testIdentifier))
.setTestCaseId(testTemplate
? maybeParent.map(TestIdentifier::getUniqueId)
.orElseGet(testIdentifier::getUniqueId)
: testIdentifier.getUniqueId()
)
.setTestCaseName(testTemplate
? maybeParent.map(TestIdentifier::getDisplayName)
.orElseGet(testIdentifier::getDisplayName)
: testIdentifier.getDisplayName())
.setHistoryId(getHistoryId(testIdentifier))
.setStage(Stage.RUNNING);
if (testTemplate) {
// history id is ignored in Allure TestOps, so we add a hidden parameter
// to make sure different results are not considered as retries
result.getParameters().add(new Parameter()
.setMode(Parameter.Mode.HIDDEN)
.setName("UniqueId")
.setValue(testIdentifier.getUniqueId())
);
}
result.getLabels().addAll(getProvidedLabels());
result.getLabels().add(getJUnitPlatformUniqueId(testIdentifier));
// add annotations from outer classes (support for @Nested tests in JUnit 5)
testClass.ifPresent(clazz -> {
Class> clazz1 = clazz;
do {
final Set