All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.palantir.tracing.TestTracingExtension Maven / Gradle / Ivy

The newest version!
/*
 * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
 *
 * 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 com.palantir.tracing;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.palantir.tracing.api.Serialization;
import com.palantir.tracing.api.Span;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.AnnotationSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class TestTracingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final Logger log = LoggerFactory.getLogger(TestTracingExtension.class);
    private final TestTracingSubscriber subscriber = new TestTracingSubscriber();

    @Override
    public void beforeTestExecution(ExtensionContext context) {
        Tracer.setSampler(AlwaysSampler.INSTANCE);
        Tracer.subscribe(context.getUniqueId(), subscriber);

        // TODO(dfox): sample can be modified by other code, we should be try ensure that the trace is always sampled
        // for the lifetime of the test
        // TODO(dfox): clear existing tracing??
        // TODO(forozco): cleanup stale snapshots from outdated tests cases/classes
    }

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        String name = testName(context);
        Tracer.unsubscribe(context.getUniqueId());

        Path outputPath = getOutputPath(name);
        Path snapshotFile = Paths.get("src/test/resources/tracing").resolve(name + ".log");
        Files.createDirectories(outputPath);
        Path actualPath = outputPath.resolve("actual.html");
        Path expectedPath = outputPath.resolve("expected.html");

        TestTracing annotation = AnnotationSupport.findAnnotation(context.getRequiredTestMethod(), TestTracing.class)
                .orElseThrow(() -> new RuntimeException("Expected " + name + " to be annotated with @TestTracing"));

        Collection actualSpans = subscriber.getAllSpans();

        if (!annotation.snapshot()) {
            HtmlFormatter.render(HtmlFormatter.RenderConfig.builder()
                    .spans(actualSpans)
                    .path(actualPath)
                    .displayName(name)
                    .layoutStrategy(annotation.layout())
                    .build());
            log.info("Tracing report file://{}", actualPath.toAbsolutePath());
            return;
        }

        // match recorded traces against expected file (or create)
        if (!Files.exists(snapshotFile) || Boolean.valueOf(System.getProperty("recreate", "false"))) {
            Serialization.serialize(snapshotFile, actualSpans);
            HtmlFormatter.render(HtmlFormatter.RenderConfig.builder()
                    .spans(actualSpans)
                    .path(actualPath)
                    .displayName(name)
                    .layoutStrategy(annotation.layout())
                    .build());
            log.info("Tracing report file://{}", actualPath.toAbsolutePath());
            return;
        }

        // TODO(df0x): filter for just one traceId (??) to figure out concurrency
        List expectedSpans = Serialization.deserialize(snapshotFile);
        SpanAnalyzer.Result expected = SpanAnalyzer.analyze(expectedSpans);
        SpanAnalyzer.Result actual = SpanAnalyzer.analyze(actualSpans);

        Set failures = SpanAnalyzer.compareSpansRecursively(
                        expected, actual, expected.root(), actual.root())
                .collect(ImmutableSet.toImmutableSet());

        HtmlFormatter.render(HtmlFormatter.RenderConfig.builder()
                .spans(actualSpans)
                .path(actualPath)
                .displayName("actual")
                .problemSpanIds(failures.stream()
                        .map(res -> res.map(
                                ComparisonFailure.unequalOperation::expected,
                                ComparisonFailure.unequalChildren::expected,
                                ComparisonFailure.incompatibleStructure::expected))
                        .map(Span::getSpanId)
                        .collect(ImmutableSet.toImmutableSet()))
                .layoutStrategy(annotation.layout())
                .build());

        HtmlFormatter.render(HtmlFormatter.RenderConfig.builder()
                .spans(expectedSpans)
                .path(expectedPath)
                .displayName("expected")
                .problemSpanIds(failures.stream()
                        .map(res -> res.map(
                                ComparisonFailure.unequalOperation::actual,
                                ComparisonFailure.unequalChildren::actual,
                                ComparisonFailure.incompatibleStructure::actual))
                        .map(Span::getSpanId)
                        .collect(ImmutableSet.toImmutableSet()))
                .layoutStrategy(annotation.layout())
                .build());

        if (!failures.isEmpty()) {
            throw new AssertionError(String.format(
                    "Traces did not match the expected file '%s'.\n"
                            + "%s\n"
                            + "Visually Compare:\n"
                            + " - expected: file://%s\n"
                            + " - actual:   file://%s\n"
                            + "Or re-run with -Drecreate=true to accept the new behaviour.",
                    snapshotFile,
                    failures.stream().map(TestTracingExtension::renderFailure).collect(Collectors.joining("\n")),
                    expectedPath.toAbsolutePath(),
                    actualPath.toAbsolutePath()));
        }
    }

    private static String renderFailure(ComparisonFailure failure) {
        return failure.map(
                (ComparisonFailure.unequalOperation t) -> String.format(
                        "Expected operation %s but received %s",
                        t.expected().getOperation(), t.actual().getOperation()),
                (ComparisonFailure.unequalChildren t) -> String.format(
                        "Expected children with operations %s but received %s",
                        t.expectedChildren().stream().map(Span::getOperation).collect(ImmutableList.toImmutableList()),
                        t.actualChildren().stream().map(Span::getOperation).collect(ImmutableList.toImmutableList())),
                (ComparisonFailure.incompatibleStructure _t) ->
                        String.format("Expected children to structured similarly"));
    }

    private static String testName(ExtensionContext context) {
        Deque segments = new ArrayDeque<>();
        for (ExtensionContext foo = context; foo != null; foo = foo.getParent().orElse(null)) {
            segments.push(foo.getDisplayName());
        }

        return segments.stream()
                .filter(s -> !s.equals("JUnit Jupiter"))
                .map(s -> s.replaceAll("\\(.*\\)$", ""))
                .collect(Collectors.joining("/"));
    }

    private static Path getOutputPath(String name) {
        String circleArtifactsDir = System.getenv("CIRCLE_ARTIFACTS");
        if (circleArtifactsDir == null) {
            return Paths.get("build/reports/tracing").resolve(name);
        }

        return Paths.get(circleArtifactsDir).resolve(name);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy