io.qameta.allure.allure2.Allure2Plugin Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of allure-generator Show documentation
Show all versions of allure-generator Show documentation
Module allure-generator of Allure Framework.
/*
* 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.allure2;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.qameta.allure.Reader;
import io.qameta.allure.context.RandomUidContext;
import io.qameta.allure.core.Configuration;
import io.qameta.allure.core.ResultsVisitor;
import io.qameta.allure.entity.Attachment;
import io.qameta.allure.entity.Label;
import io.qameta.allure.entity.Link;
import io.qameta.allure.entity.Parameter;
import io.qameta.allure.entity.StageResult;
import io.qameta.allure.entity.Status;
import io.qameta.allure.entity.Step;
import io.qameta.allure.entity.Time;
import io.qameta.allure.model.FixtureResult;
import io.qameta.allure.model.StepResult;
import io.qameta.allure.model.TestResult;
import io.qameta.allure.model.TestResultContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static io.qameta.allure.entity.LabelName.RESULT_FORMAT;
import static io.qameta.allure.model.Parameter.Mode.HIDDEN;
import static io.qameta.allure.model.Parameter.Mode.MASKED;
import static io.qameta.allure.util.ConvertUtils.convertList;
import static java.nio.file.Files.newDirectoryStream;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
import static java.util.Comparator.nullsLast;
import static java.util.Objects.nonNull;
/**
* Plugin that reads results from Allure 2 data format.
*
* @since 2.0
*/
@SuppressWarnings({
"ClassDataAbstractionCoupling",
"ClassFanOutComplexity",
"PMD.ExcessiveImports",
"PMD.TooManyMethods",
})
public class Allure2Plugin implements Reader {
@SuppressWarnings("WeakerAccess")
public static final String ALLURE2_RESULTS_FORMAT = "allure2";
private static final Logger LOGGER = LoggerFactory.getLogger(Allure2Plugin.class);
private static final Comparator BY_START = comparing(
StageResult::getTime,
nullsLast(comparing(Time::getStart, nullsLast(naturalOrder())))
);
private final ObjectMapper mapper = JsonMapper.builder()
.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
.disable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
.build();
@Override
public void readResults(final Configuration configuration,
final ResultsVisitor visitor,
final Path resultsDirectory) {
final RandomUidContext context = configuration.requireContext(RandomUidContext.class);
final Map> befores = new ConcurrentHashMap<>();
final Map> afters = new ConcurrentHashMap<>();
readTestResultsContainers(resultsDirectory)
.filter(group -> !Objects.isNull(group.getChildren()))
.forEach(group -> {
processStages(visitor, resultsDirectory, group, befores, group.getBefores());
processStages(visitor, resultsDirectory, group, afters, group.getAfters());
});
sortByStart(befores);
sortByStart(afters);
readTestResults(resultsDirectory)
.forEach(result -> convert(
context.getValue(),
resultsDirectory, visitor,
result,
befores, afters
));
}
private static void sortByStart(final Map> befores) {
befores.keySet().forEach(key -> befores.compute(key, (s, stageResults) -> {
if (Objects.isNull(stageResults)) {
return null;
}
final List res = new ArrayList<>(stageResults);
res.sort(BY_START);
return res;
}));
}
private void processStages(final ResultsVisitor visitor,
final Path resultsDirectory,
final TestResultContainer group,
final Map> befores,
final List fixtureResults) {
if (Objects.isNull(fixtureResults)) {
return;
}
final List stages = fixtureResults.stream()
.map(fixtureResult -> convert(resultsDirectory, visitor, fixtureResult))
.collect(Collectors.toList());
final Set visited = ConcurrentHashMap.newKeySet();
group.getChildren()
.parallelStream()
.filter(Objects::nonNull)
.filter(visited::add)
.forEach(child -> befores.compute(child, (s, stageResults) -> {
if (Objects.isNull(stageResults)) {
return new LinkedList<>(stages);
}
stageResults.addAll(stages);
return stageResults;
}));
}
private void convert(final Supplier uidGenerator,
final Path resultsDirectory,
final ResultsVisitor visitor,
final TestResult result,
final Map> befores,
final Map> afters) {
final io.qameta.allure.entity.TestResult dest = new io.qameta.allure.entity.TestResult();
dest.setUid(uidGenerator.get());
dest.setHistoryId(result.getHistoryId());
dest.setFullName(result.getFullName());
dest.setName(firstNonNull(result.getName(), result.getFullName(), "Unknown test"));
dest.setTime(Time.create(result.getStart(), result.getStop()));
dest.setDescription(result.getDescription());
dest.setDescriptionHtml(result.getDescriptionHtml());
dest.setStatus(convert(result.getStatus()));
Optional.ofNullable(result.getStatusDetails()).ifPresent(details -> {
dest.setStatusMessage(details.getMessage());
dest.setStatusTrace(details.getTrace());
dest.setFlaky(details.isFlaky());
});
dest.setLinks(convertList(result.getLinks(), this::convert));
dest.setLabels(convertList(result.getLabels(), this::convert));
dest.setParameters(getParameters(result));
dest.addLabelIfNotExists(RESULT_FORMAT, ALLURE2_RESULTS_FORMAT);
if (hasTestStage(result)) {
dest.setTestStage(getTestStage(resultsDirectory, visitor, result));
}
if (nonNull(result.getUuid())) {
final List resultBefores = befores.get(result.getUuid());
if (nonNull(resultBefores)) {
dest.getBeforeStages().addAll(resultBefores);
}
final List resultAfters = afters.get(result.getUuid());
if (nonNull(resultAfters)) {
dest.getAfterStages().addAll(resultAfters);
}
}
visitor.visitTestResult(dest);
}
private StageResult convert(final Path source,
final ResultsVisitor visitor,
final FixtureResult result) {
final StageResult stageResult = new StageResult()
.setName(result.getName())
.setTime(convert(result.getStart(), result.getStop()))
.setStatus(convert(result.getStatus()))
.setSteps(convertList(result.getSteps(), step -> convert(source, visitor, step)))
.setDescription(result.getDescription())
.setDescriptionHtml(result.getDescriptionHtml())
.setAttachments(convertList(result.getAttachments(), attach -> convert(source, visitor, attach)))
.setParameters(convertList(result.getParameters(), p -> !HIDDEN.equals(p.getMode()), this::convert));
Optional.of(result)
.map(FixtureResult::getStatusDetails)
.ifPresent(statusDetails -> {
stageResult.setStatusMessage(statusDetails.getMessage());
stageResult.setStatusTrace(statusDetails.getTrace());
});
return stageResult;
}
private Link convert(final io.qameta.allure.model.Link link) {
return new Link()
.setName(link.getName())
.setType(link.getType())
.setUrl(link.getUrl());
}
private Label convert(final io.qameta.allure.model.Label label) {
return new Label()
.setName(label.getName())
.setValue(label.getValue());
}
private Parameter convert(final io.qameta.allure.model.Parameter parameter) {
final boolean masked = MASKED.equals(parameter.getMode());
return new Parameter()
.setName(parameter.getName())
.setValue(masked ? "******" : parameter.getValue());
}
private Attachment convert(final Path source,
final ResultsVisitor visitor,
final io.qameta.allure.model.Attachment attachment) {
final Path attachmentFile = source.resolve(attachment.getSource());
if (Files.isRegularFile(attachmentFile)) {
final Attachment found = visitor.visitAttachmentFile(attachmentFile);
if (nonNull(attachment.getType())) {
found.setType(attachment.getType());
}
if (nonNull(attachment.getName())) {
found.setName(attachment.getName());
}
return found;
} else {
visitor.error("Could not find attachment " + attachment.getSource() + " in directory " + source);
return new Attachment()
.setType(attachment.getType())
.setName(attachment.getName())
.setSize(0L);
}
}
private Step convert(final Path source,
final ResultsVisitor visitor,
final StepResult step) {
final Step result = new Step()
.setName(step.getName())
.setStatus(convert(step.getStatus()))
.setTime(convert(step.getStart(), step.getStop()))
.setParameters(convertList(step.getParameters(), p -> !HIDDEN.equals(p.getMode()), this::convert))
.setAttachments(convertList(step.getAttachments(), attachment -> convert(source, visitor, attachment)))
.setSteps(convertList(step.getSteps(), s -> convert(source, visitor, s)));
Optional.of(step)
.map(StepResult::getStatusDetails)
.ifPresent(statusDetails -> {
result.setStatusMessage(statusDetails.getMessage());
result.setStatusTrace(statusDetails.getTrace());
});
return result;
}
private Status convert(final io.qameta.allure.model.Status status) {
if (Objects.isNull(status)) {
return Status.UNKNOWN;
}
return Stream.of(Status.values())
.filter(item -> item.value().equalsIgnoreCase(status.value()))
.findAny()
.orElse(Status.UNKNOWN);
}
private Time convert(final Long start, final Long stop) {
return new Time()
.setStart(start)
.setStop(stop)
.setDuration(nonNull(start) && nonNull(stop) ? stop - start : null);
}
private List getParameters(final TestResult result) {
final TreeSet parametersSet = new TreeSet<>(
comparing(Parameter::getName, nullsFirst(naturalOrder()))
.thenComparing(Parameter::getValue, nullsFirst(naturalOrder()))
);
parametersSet.addAll(convertList(result.getParameters(), p -> !HIDDEN.equals(p.getMode()), this::convert));
return new ArrayList<>(parametersSet);
}
private StageResult getTestStage(final Path source,
final ResultsVisitor visitor,
final TestResult result) {
final StageResult testStage = new StageResult();
testStage.setSteps(convertList(
result.getSteps(),
step -> convert(source, visitor, step)
));
testStage.setAttachments(convertList(
result.getAttachments(),
attachment -> convert(source, visitor, attachment)
));
testStage.setStatus(convert(result.getStatus()));
testStage.setDescription(result.getDescription());
testStage.setDescriptionHtml(result.getDescriptionHtml());
Optional.of(result)
.map(TestResult::getStatusDetails)
.ifPresent(statusDetails -> {
testStage.setStatusMessage(statusDetails.getMessage());
testStage.setStatusTrace(statusDetails.getTrace());
});
return testStage;
}
private boolean hasTestStage(final TestResult result) {
return !result.getSteps().isEmpty() || !result.getAttachments().isEmpty();
}
@SafeVarargs
private static T firstNonNull(final T... items) {
return Stream.of(items)
.filter(Objects::nonNull)
.findFirst()
.orElseThrow(() -> new IllegalStateException(
"firstNonNull method should have at least one non null parameter"
));
}
private Stream readTestResultsContainers(final Path resultsDirectory) {
return listFiles(resultsDirectory, "*-container.json")
.parallel()
.map(this::readTestResultContainer)
.filter(Optional::isPresent)
.map(Optional::get);
}
private Stream readTestResults(final Path resultsDirectory) {
return listFiles(resultsDirectory, "*-result.json")
.parallel()
.map(this::readTestResult)
.filter(Optional::isPresent)
.map(Optional::get);
}
private Optional readTestResult(final Path file) {
try (InputStream is = Files.newInputStream(file)) {
return Optional.ofNullable(mapper.readValue(is, TestResult.class));
} catch (IOException e) {
LOGGER.error("Could not read test result file {}", file, e);
return Optional.empty();
}
}
private Optional readTestResultContainer(final Path file) {
try (InputStream is = Files.newInputStream(file)) {
return Optional.ofNullable(mapper.readValue(is, TestResultContainer.class));
} catch (IOException e) {
LOGGER.error("Could not read result container file {}", file, e);
return Optional.empty();
}
}
private Stream listFiles(final Path directory, final String glob) {
try (DirectoryStream directoryStream = newDirectoryStream(directory, glob)) {
return StreamSupport.stream(directoryStream.spliterator(), true)
.filter(Files::isRegularFile)
.collect(Collectors.toList())
.stream();
} catch (IOException e) {
LOGGER.error("Could not list files in directory {}", directory, e);
return Stream.empty();
}
}
}