From 4654d52f319967335cec0bc36ec10e144a836894 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 16 Dec 2025 10:45:24 +0100 Subject: [PATCH 1/3] feat(test): Add instrumentation test for JUnit --- .../test/AbstractInstrumentationTest.java | 40 +++++- .../trace/agent/test/assertions/Is.java | 26 ++++ .../trace/agent/test/assertions/Matcher.java | 10 ++ .../trace/agent/test/assertions/Matchers.java | 26 ++++ .../trace/agent/test/assertions/Matches.java | 27 ++++ .../trace/agent/test/assertions/NonNull.java | 20 +++ .../agent/test/assertions/SpanMatcher.java | 127 ++++++++++++++++++ .../test/assertions/TraceAssertions.java | 103 ++++++++++++++ .../agent/test/assertions/TraceMatcher.java | 72 ++++++++++ .../agent/test/assertions/Validates.java | 27 ++++ .../VirtualThreadApiInstrumentationTest.java | 47 ++----- 11 files changed, 491 insertions(+), 34 deletions(-) create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Is.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matcher.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matches.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceMatcher.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Validates.java diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java index 17686a7d326..c7c79d045b1 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java @@ -1,9 +1,12 @@ package datadog.trace.agent.test; +import static java.util.function.Function.identity; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.instrument.classinject.ClassInjector; +import datadog.trace.agent.test.assertions.TraceAssertions; +import datadog.trace.agent.test.assertions.TraceMatcher; import datadog.trace.agent.tooling.AgentInstaller; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.agent.tooling.TracerInstaller; @@ -22,11 +25,19 @@ import java.util.ServiceLoader; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Function; import net.bytebuddy.agent.ByteBuddyAgent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; - +import org.opentest4j.AssertionFailedError; + +/** + * This class is an experimental base to run instrumentation tests using JUnit Jupiter. It is still + * early development, and the overall API is expected to change to leverage its extension model. The + * current implementation is inspired and kept close to it Groovy / Spock counterpart, the {@code + * InstrumentationSpecification}. + */ @ExtendWith(TestClassShadowingExtension.class) public abstract class AbstractInstrumentationTest { static final Instrumentation INSTRUMENTATION = ByteBuddyAgent.getInstrumentation(); @@ -100,6 +111,33 @@ public void tearDown() { this.transformerLister = null; } + /** + * Checks the structure of the traces captured from the test agent. + * + * @param matchers The matchers to verify the trace collection, one matcher by expected trace. + */ + protected void assertTraces(TraceMatcher... matchers) { + assertTraces(identity(), matchers); + } + + /** + * Checks the structure of the traces captured from the test agent. + * + * @param options The {@link TraceAssertions.Options} to configure the checks. + * @param matchers The matchers to verify the trace collection, one matcher by expected trace. + */ + protected void assertTraces( + Function options, + TraceMatcher... matchers) { + int expectedTraceCount = matchers.length; + try { + this.writer.waitForTraces(expectedTraceCount); + } catch (InterruptedException | TimeoutException e) { + throw new AssertionFailedError("Timeout while waiting for traces", e); + } + TraceAssertions.assertTraces(this.writer, options, matchers); + } + protected void blockUntilChildSpansFinished(final int numberOfSpans) { blockUntilChildSpansFinished(this.tracer.activeSpan(), numberOfSpans); } diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Is.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Is.java new file mode 100644 index 00000000000..78f0f9da919 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Is.java @@ -0,0 +1,26 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; + +public class Is implements Matcher { + private final T expected; + + Is(T expected) { + this.expected = expected; + } + + @Override + public Optional expected() { + return Optional.of(this.expected); + } + + @Override + public String message() { + return "Unexpected value"; + } + + @Override + public boolean test(T t) { + return this.expected.equals(t); + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matcher.java new file mode 100644 index 00000000000..a020a99a82e --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matcher.java @@ -0,0 +1,10 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; +import java.util.function.Predicate; + +public interface Matcher extends Predicate { + Optional expected(); + + String message(); +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java new file mode 100644 index 00000000000..1da4836a1f6 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java @@ -0,0 +1,26 @@ +package datadog.trace.agent.test.assertions; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class Matchers { + public static Matcher is(T expected) { + return new Is<>(expected); + } + + public static Matcher nonNull() { + return new NonNull<>(); + } + + public static Matcher matches(String regex) { + return new Matches(Pattern.compile(regex)); + } + + public static Matcher matches(Pattern pattern) { + return new Matches(pattern); + } + + public static Matcher validates(Predicate validator) { + return new Validates<>(validator); + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matches.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matches.java new file mode 100644 index 00000000000..49ee6e6d3b4 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matches.java @@ -0,0 +1,27 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; +import java.util.regex.Pattern; + +public class Matches implements Matcher { + private final Pattern pattern; + + Matches(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public Optional expected() { + return Optional.empty(); + } + + @Override + public String message() { + return "Non matching value"; + } + + @Override + public boolean test(CharSequence s) { + return this.pattern.matcher(s).matches(); + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java new file mode 100644 index 00000000000..7612f89a3b4 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java @@ -0,0 +1,20 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; + +public class NonNull implements Matcher { + @Override + public Optional expected() { + return Optional.empty(); + } + + @Override + public String message() { + return "Non-null value expected"; + } + + @Override + public boolean test(T t) { + return t != null; + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java new file mode 100644 index 00000000000..b33f4fa2609 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java @@ -0,0 +1,127 @@ +package datadog.trace.agent.test.assertions; + +import static datadog.trace.agent.test.assertions.Matchers.is; +import static datadog.trace.agent.test.assertions.Matchers.matches; +import static datadog.trace.agent.test.assertions.Matchers.nonNull; +import static datadog.trace.agent.test.assertions.Matchers.validates; +import static java.time.temporal.ChronoUnit.NANOS; + +import datadog.trace.core.DDSpan; +import java.time.Duration; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import org.opentest4j.AssertionFailedError; + +public final class SpanMatcher { + private Matcher idMatcher; + private Matcher parentIdMatcher; + private Matcher serviceNameMatcher; + private Matcher operationNameMatcher; + private Matcher resourceNameMatcher; + private Matcher durationMatcher; + + private static final Matcher CHILD_OF_PREVIOUS_MATCHER = is(0L); + + private SpanMatcher() {} + + public static SpanMatcher span() { + return new SpanMatcher(); + } + + public SpanMatcher withId(long id) { + this.idMatcher = is(id); + return this; + } + + public SpanMatcher isRoot() { + return childOf(0L); + } + + public SpanMatcher childOf(long parentId) { + this.parentIdMatcher = is(parentId); + return this; + } + + public SpanMatcher childOfPrevious() { + this.parentIdMatcher = CHILD_OF_PREVIOUS_MATCHER; + return this; + } + + public SpanMatcher hasServiceName() { + this.serviceNameMatcher = nonNull(); + return this; + } + + public SpanMatcher withServiceName(String serviceName) { + this.serviceNameMatcher = is(serviceName); + return this; + } + + public SpanMatcher withOperationName(String operationName) { + this.operationNameMatcher = is(operationName); + return this; + } + + public SpanMatcher operationNameMatching(Pattern pattern) { + this.operationNameMatcher = matches(pattern); + return this; + } + + public SpanMatcher withResourceName(String resourceName) { + this.resourceNameMatcher = is(resourceName); + return this; + } + + public SpanMatcher resourceNameMatching(Pattern pattern) { + this.resourceNameMatcher = matches(pattern); + return this; + } + + public SpanMatcher resourceNameMatching(Predicate validator) { + this.resourceNameMatcher = validates(validator); + return this; + } + + public SpanMatcher durationShorterThan(Duration duration) { + this.durationMatcher = validates(d -> d.compareTo(duration) < 0); + return this; + } + + public SpanMatcher durationLongerThan(Duration duration) { + this.durationMatcher = validates(d -> d.compareTo(duration) > 0); + return this; + } + + public SpanMatcher durationMatching(Predicate validator) { + this.durationMatcher = validates(validator); + return this; + } + + public void assertSpan(DDSpan span, DDSpan previousSpan) { + // Apply parent id matcher from the previous span + if (this.parentIdMatcher == CHILD_OF_PREVIOUS_MATCHER) { + this.parentIdMatcher = is(previousSpan.getSpanId()); + } + // Assert span values + assertValue(this.idMatcher, span.getSpanId(), "Expected identifier"); + assertValue(this.parentIdMatcher, span.getParentId(), "Expected parent identifier"); + assertValue(this.serviceNameMatcher, span.getServiceName(), "Expected service name"); + assertValue(this.operationNameMatcher, span.getOperationName(), "Expected operation name"); + assertValue(this.resourceNameMatcher, span.getResourceName(), "Expected resource name"); + assertValue( + this.durationMatcher, Duration.of(span.getDurationNano(), NANOS), "Expected duration"); + // TODO Add more values to test (tags, links, ...) + } + + private void assertValue(Matcher matcher, T value, String message) { + if (matcher != null && !matcher.test(value)) { + Optional expected = matcher.expected(); + if (expected.isPresent()) { + throw new AssertionFailedError(message + ". " + matcher.message(), expected.get(), value); + } else { + throw new AssertionFailedError(message + ": " + value + ". " + matcher.message()); + } + } + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java new file mode 100644 index 00000000000..491e5a87e84 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java @@ -0,0 +1,103 @@ +package datadog.trace.agent.test.assertions; + +import static java.util.function.Function.identity; + +import datadog.trace.core.DDSpan; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; +import org.opentest4j.AssertionFailedError; + +public final class TraceAssertions { + /** Trace comparator to sort by start time. */ + public static final Comparator> TRACE_START_TIME_COMPARATOR = + Comparator.comparingLong( + trace -> trace.isEmpty() ? 0L : trace.get(0).getLocalRootSpan().getStartTime()); + + /** Trace comparator to sort by root span identifier. */ + public static final Comparator> TRACE_ROOT_SPAN_ID_COMPARATOR = + Comparator.comparingLong( + trace -> trace.isEmpty() ? 0L : trace.get(0).getLocalRootSpan().getSpanId()); + + /* + * Trace assertions options. + */ + /** Ignores addition traces. If there are more traces than expected, do not fail. */ + public static final Function IGNORE_ADDITIONAL_TRACES = + Options::ignoredAdditionalTraces; + + /** Sorts traces by start time. */ + public static final Function SORT_BY_START_TIME = + options -> options.sorter(TRACE_START_TIME_COMPARATOR); + + /** Sorts traces by their root span identifier. */ + public static final Function SORT_BY_ROOT_SPAN_ID = + options -> options.sorter(TRACE_ROOT_SPAN_ID_COMPARATOR); + + private TraceAssertions() {} + + /** + * Checks a trace structure. + * + * @param trace The trace to check. + * @param matcher The matcher to verify the trace structure. + */ + public static void assertTrace(List trace, TraceMatcher matcher) { + matcher.assertTrace(trace, 0); + } + + /** + * Checks the structure of a trace collection. + * + * @param traces The trace collection to check. + * @param matchers The matchers to verify the trace collection, one matcher by expected trace. + */ + public static void assertTraces(List> traces, TraceMatcher... matchers) { + assertTraces(traces, identity(), matchers); + } + + /** + * Checks the structure of a trace collection. + * + * @param traces The trace collection to check. + * @param options The {@link Options} to configure the checks. + * @param matchers The matchers to verify the trace collection, one matcher by expected trace. + */ + public static void assertTraces( + List> traces, Function options, TraceMatcher... matchers) { + Options opts = options.apply(new Options()); + int expectedTraceCount = matchers.length; + int traceCount = traces.size(); + if (opts.ignoredAdditionalTraces) { + if (traceCount < expectedTraceCount) { + throw new AssertionFailedError("Not enough of traces", expectedTraceCount, traceCount); + } + } else { + if (traceCount != expectedTraceCount) { + throw new AssertionFailedError("Invalid number of traces", expectedTraceCount, traceCount); + } + } + if (opts.sorter != null) { + traces.sort(opts.sorter); + } + for (int i = 0; i < expectedTraceCount; i++) { + List trace = traces.get(i); + matchers[i].assertTrace(trace, i); + } + } + + public static class Options { + boolean ignoredAdditionalTraces = false; + Comparator> sorter = TRACE_START_TIME_COMPARATOR; + + public Options ignoredAdditionalTraces() { + this.ignoredAdditionalTraces = true; + return this; + } + + public Options sorter(Comparator> sorter) { + this.sorter = sorter; + return this; + } + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceMatcher.java new file mode 100644 index 00000000000..0666aa758df --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceMatcher.java @@ -0,0 +1,72 @@ +package datadog.trace.agent.test.assertions; + +import datadog.trace.core.DDSpan; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; +import org.opentest4j.AssertionFailedError; + +public final class TraceMatcher { + public static final Comparator START_TIME_COMPARATOR = + Comparator.comparingLong(DDSpan::getStartTime); + public static Function SORT_BY_START_TIME = + options -> options.sorter(START_TIME_COMPARATOR); + + private final Options options; + private final SpanMatcher[] matchers; + + private TraceMatcher(Options options, SpanMatcher[] matchers) { + if (matchers.length == 0) { + throw new IllegalArgumentException("No span matchers provided"); + } + this.options = options; + this.matchers = matchers; + } + + /** + * Checks a trace structure. + * + * @param matchers The matchers to verify the trace structure. + */ + public static TraceMatcher trace(SpanMatcher... matchers) { + return new TraceMatcher(new Options(), matchers); + } + + /** + * Checks a trace structure. + * + * @param options The {@link TraceAssertions.Options} to configure the checks. + * @param matchers The matchers to verify the trace structure. + */ + public static TraceMatcher trace(Function options, SpanMatcher... matchers) { + return new TraceMatcher(options.apply(new Options()), matchers); + } + + void assertTrace(List trace, int traceIndex) { + int spanCount = trace.size(); + if (spanCount != this.matchers.length) { + throw new AssertionFailedError( + "Invalid number of spans for trace " + traceIndex + " : " + trace, + this.matchers.length, + spanCount); + } + if (this.options.sorter != null) { + trace.sort(this.options.sorter); + } + DDSpan previousSpan = null; + for (int i = 0; i < spanCount; i++) { + DDSpan span = trace.get(i); + this.matchers[i].assertSpan(span, previousSpan); + previousSpan = span; + } + } + + public static class Options { + Comparator sorter = null; + + public Options sorter(Comparator sorter) { + this.sorter = sorter; + return this; + } + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Validates.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Validates.java new file mode 100644 index 00000000000..0aec805b55b --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Validates.java @@ -0,0 +1,27 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; +import java.util.function.Predicate; + +public class Validates implements Matcher { + private final Predicate validator; + + public Validates(Predicate validator) { + this.validator = validator; + } + + @Override + public Optional expected() { + return Optional.empty(); + } + + @Override + public String message() { + return "Invalid value"; + } + + @Override + public boolean test(T t) { + return this.validator.test(t); + } +} diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java index f702400a7a7..b0c060b9df6 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java @@ -1,14 +1,11 @@ package testdog.trace.instrumentation.java.lang.jdk21; -import static java.util.Collections.emptyList; -import static java.util.Comparator.comparing; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; import datadog.trace.agent.test.AbstractInstrumentationTest; import datadog.trace.api.Trace; -import datadog.trace.core.DDSpan; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeoutException; @@ -135,36 +132,20 @@ public void run() { latch.await(); - var trace = getTrace(); - trace.sort(comparing(DDSpan::getStartTimeNano)); - assertEquals(4, trace.size()); - assertEquals("parent", trace.get(0).getOperationName()); - assertEquals("child", trace.get(1).getOperationName()); - assertEquals("great-child", trace.get(2).getOperationName()); - assertEquals("great-great-child", trace.get(3).getOperationName()); - assertEquals(trace.get(0).getSpanId(), trace.get(1).getParentId()); - assertEquals(trace.get(1).getSpanId(), trace.get(2).getParentId()); - assertEquals(trace.get(2).getSpanId(), trace.get(3).getParentId()); + assertTraces( + trace( + SORT_BY_START_TIME, + span().isRoot().withOperationName("parent"), + span().childOfPrevious().withOperationName("child"), + span().childOfPrevious().withOperationName("great-child"), + span().childOfPrevious().withOperationName("great-great-child"))); } /** Verifies the parent / child span relation. */ void assertConnectedTrace() { - var trace = getTrace(); - trace.sort(comparing(DDSpan::getStartTimeNano)); - assertEquals(2, trace.size()); - assertEquals("parent", trace.get(0).getOperationName()); - assertEquals("asyncChild", trace.get(1).getOperationName()); - assertEquals(trace.get(0).getSpanId(), trace.get(1).getParentId()); - } - - List getTrace() { - try { - writer.waitForTraces(1); - assertEquals(1, writer.size()); - return writer.getFirst(); - } catch (InterruptedException | TimeoutException e) { - fail("Failed to wait for trace to finish.", e); - return emptyList(); - } + assertTraces( + trace( + span().isRoot().withOperationName("parent"), + span().childOfPrevious().withOperationName("asyncChild"))); } } From 1bf9c7f2d9c0b7011b051dd38f90db649d345efb Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Mon, 22 Dec 2025 14:33:23 +0100 Subject: [PATCH 2/3] feat(test): Add instrumentation test for JUnit --- .../trace/agent/test/assertions/Any.java | 20 ++++ .../trace/agent/test/assertions/IsFalse.java | 20 ++++ .../{NonNull.java => IsNonNull.java} | 2 +- .../trace/agent/test/assertions/IsNull.java | 20 ++++ .../trace/agent/test/assertions/IsTrue.java | 20 ++++ .../trace/agent/test/assertions/Matchers.java | 38 ++++++- .../test/assertions/SpanLinkMatcher.java | 68 +++++++++++++ .../agent/test/assertions/SpanMatcher.java | 98 ++++++++++++++----- .../datadog/trace/core/DDSpanAccessor.java | 16 +++ .../VirtualThreadApiInstrumentationTest.java | 12 +-- 10 files changed, 281 insertions(+), 33 deletions(-) create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Any.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsFalse.java rename dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/{NonNull.java => IsNonNull.java} (85%) create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNull.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsTrue.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanLinkMatcher.java create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/core/DDSpanAccessor.java diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Any.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Any.java new file mode 100644 index 00000000000..ea1859d3996 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Any.java @@ -0,0 +1,20 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; + +public class Any implements Matcher { + @Override + public Optional expected() { + return Optional.empty(); + } + + @Override + public String message() { + return ""; + } + + @Override + public boolean test(T t) { + return true; + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsFalse.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsFalse.java new file mode 100644 index 00000000000..caa98eddbc7 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsFalse.java @@ -0,0 +1,20 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; + +public class IsFalse implements Matcher { + @Override + public Optional expected() { + return Optional.of(false); + } + + @Override + public String message() { + return "False expected"; + } + + @Override + public boolean test(Boolean t) { + return !t; + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNonNull.java similarity index 85% rename from dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java rename to dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNonNull.java index 7612f89a3b4..7ab082eb94d 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/NonNull.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNonNull.java @@ -2,7 +2,7 @@ import java.util.Optional; -public class NonNull implements Matcher { +public class IsNonNull implements Matcher { @Override public Optional expected() { return Optional.empty(); diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNull.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNull.java new file mode 100644 index 00000000000..4c2ca637944 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsNull.java @@ -0,0 +1,20 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; + +public class IsNull implements Matcher { + @Override + public Optional expected() { + return Optional.empty(); + } + + @Override + public String message() { + return "Null value expected"; + } + + @Override + public boolean test(T t) { + return t == null; + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsTrue.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsTrue.java new file mode 100644 index 00000000000..4aeb1421e89 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/IsTrue.java @@ -0,0 +1,20 @@ +package datadog.trace.agent.test.assertions; + +import java.util.Optional; + +public class IsTrue implements Matcher { + @Override + public Optional expected() { + return Optional.of(true); + } + + @Override + public String message() { + return "True expected"; + } + + @Override + public boolean test(Boolean t) { + return t; + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java index 1da4836a1f6..f2a98104595 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java @@ -1,15 +1,36 @@ package datadog.trace.agent.test.assertions; +import java.util.Optional; import java.util.function.Predicate; import java.util.regex.Pattern; +import org.opentest4j.AssertionFailedError; + +/* + * TODO: Dev notes + * - introduce as few as possible matchers + * - only have matchers for generic purpose, don't introduce feature / produce / use-case specific matchers + * - name "ignores" as "any"? + */ public class Matchers { public static Matcher is(T expected) { return new Is<>(expected); } + public static Matcher isNull() { + return new IsNull<>(); + } + public static Matcher nonNull() { - return new NonNull<>(); + return new IsNonNull<>(); + } + + public static Matcher isTrue() { + return new IsTrue(); + } + + public static Matcher isFalse() { + return new IsFalse(); } public static Matcher matches(String regex) { @@ -23,4 +44,19 @@ public static Matcher matches(Pattern pattern) { public static Matcher validates(Predicate validator) { return new Validates<>(validator); } + + public static Matcher any() { + return new Any<>(); + } + + static void assertValue(Matcher matcher, T value, String message) { + if (matcher != null && !matcher.test(value)) { + Optional expected = matcher.expected(); + if (expected.isPresent()) { + throw new AssertionFailedError(message + ". " + matcher.message(), expected.get(), value); + } else { + throw new AssertionFailedError(message + ": " + value + ". " + matcher.message()); + } + } + } } diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanLinkMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanLinkMatcher.java new file mode 100644 index 00000000000..18b7188bc96 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanLinkMatcher.java @@ -0,0 +1,68 @@ +package datadog.trace.agent.test.assertions; + +import static datadog.trace.agent.test.assertions.Matchers.assertValue; +import static datadog.trace.agent.test.assertions.Matchers.is; +import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.DEFAULT_FLAGS; +import static datadog.trace.bootstrap.instrumentation.api.SpanAttributes.EMPTY; + +import datadog.trace.api.DDTraceId; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; +import datadog.trace.core.DDSpan; + +public final class SpanLinkMatcher { + private final Matcher traceIdMatcher; + private final Matcher spanIdMatcher; + private Matcher traceFlagsMatcher; + private Matcher spanAttributesMatcher; + private Matcher traceStateMatcher; + + private SpanLinkMatcher(Matcher traceIdMatcher, Matcher spanIdMatcher) { + this.traceIdMatcher = traceIdMatcher; + this.spanIdMatcher = spanIdMatcher; + this.traceFlagsMatcher = is(DEFAULT_FLAGS); + this.spanAttributesMatcher = is(EMPTY); + this.traceStateMatcher = is(""); + } + + public static SpanLinkMatcher from(DDSpan span) { + return from(span.context()); + } + + public static SpanLinkMatcher from(AgentSpanContext spanContext) { + return from(spanContext.getTraceId(), spanContext.getSpanId()); + } + + public static SpanLinkMatcher from(DDTraceId traceId, long spanId) { + return new SpanLinkMatcher(is(traceId), is(spanId)); + } + + public static SpanLinkMatcher any() { + return new SpanLinkMatcher(Matchers.any(), Matchers.any()); + } + + public SpanLinkMatcher traceFlags(byte traceFlags) { + this.traceFlagsMatcher = is(traceFlags); + return this; + } + + public SpanLinkMatcher attributes(SpanAttributes spanAttributes) { + this.spanAttributesMatcher = is(spanAttributes); + return this; + } + + public SpanLinkMatcher traceState(String traceState) { + this.traceStateMatcher = is(traceState); + return this; + } + + void assertLink(AgentSpanLink link) { + // Assert link values + assertValue(this.traceIdMatcher, link.traceId(), "Expected trace identifier"); + assertValue(this.spanIdMatcher, link.spanId(), "Expected span identifier"); + assertValue(this.traceFlagsMatcher, link.traceFlags(), "Expected trace flags"); + assertValue(this.spanAttributesMatcher, link.attributes(), "Expected attributes"); + assertValue(this.traceStateMatcher, link.traceState(), "Expected trace state"); + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java index b33f4fa2609..e73d321cd94 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java @@ -1,18 +1,31 @@ package datadog.trace.agent.test.assertions; +import static datadog.trace.agent.test.assertions.Matchers.assertValue; import static datadog.trace.agent.test.assertions.Matchers.is; +import static datadog.trace.agent.test.assertions.Matchers.isFalse; +import static datadog.trace.agent.test.assertions.Matchers.isNull; +import static datadog.trace.agent.test.assertions.Matchers.isTrue; import static datadog.trace.agent.test.assertions.Matchers.matches; import static datadog.trace.agent.test.assertions.Matchers.nonNull; import static datadog.trace.agent.test.assertions.Matchers.validates; -import static java.time.temporal.ChronoUnit.NANOS; +import static datadog.trace.core.DDSpanAccessor.spanLinks; +import static java.time.Duration.ofNanos; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpan; import java.time.Duration; -import java.util.Optional; +import java.util.List; import java.util.function.Predicate; import java.util.regex.Pattern; import org.opentest4j.AssertionFailedError; +/* + * TODO: Dev notes + * - inconsistency with "()" vs "has()" + * - hasServiceName / withError / withoutError + */ + public final class SpanMatcher { private Matcher idMatcher; private Matcher parentIdMatcher; @@ -20,16 +33,23 @@ public final class SpanMatcher { private Matcher operationNameMatcher; private Matcher resourceNameMatcher; private Matcher durationMatcher; + private Matcher typeMatcher; + private Matcher errorMatcher; + private SpanLinkMatcher[] linkMatchers; private static final Matcher CHILD_OF_PREVIOUS_MATCHER = is(0L); - private SpanMatcher() {} + private SpanMatcher() { + this.serviceNameMatcher = validates(s -> s != null && !s.isEmpty()); + this.typeMatcher = isNull(); + this.errorMatcher = isFalse(); + } public static SpanMatcher span() { return new SpanMatcher(); } - public SpanMatcher withId(long id) { + public SpanMatcher id(long id) { this.idMatcher = is(id); return this; } @@ -53,32 +73,32 @@ public SpanMatcher hasServiceName() { return this; } - public SpanMatcher withServiceName(String serviceName) { + public SpanMatcher serviceName(String serviceName) { this.serviceNameMatcher = is(serviceName); return this; } - public SpanMatcher withOperationName(String operationName) { + public SpanMatcher operationName(String operationName) { this.operationNameMatcher = is(operationName); return this; } - public SpanMatcher operationNameMatching(Pattern pattern) { + public SpanMatcher operationName(Pattern pattern) { this.operationNameMatcher = matches(pattern); return this; } - public SpanMatcher withResourceName(String resourceName) { + public SpanMatcher resourceName(String resourceName) { this.resourceNameMatcher = is(resourceName); return this; } - public SpanMatcher resourceNameMatching(Pattern pattern) { + public SpanMatcher resourceName(Pattern pattern) { this.resourceNameMatcher = matches(pattern); return this; } - public SpanMatcher resourceNameMatching(Predicate validator) { + public SpanMatcher resourceName(Predicate validator) { this.resourceNameMatcher = validates(validator); return this; } @@ -93,12 +113,32 @@ public SpanMatcher durationLongerThan(Duration duration) { return this; } - public SpanMatcher durationMatching(Predicate validator) { + public SpanMatcher duration(Predicate validator) { this.durationMatcher = validates(validator); return this; } - public void assertSpan(DDSpan span, DDSpan previousSpan) { + public SpanMatcher type(String type) { + this.typeMatcher = is(type); + return this; + } + + public SpanMatcher withError() { + this.errorMatcher = isTrue(); + return this; + } + + public SpanMatcher withoutError() { + this.errorMatcher = isFalse(); + return this; + } + + public SpanMatcher links(SpanLinkMatcher... matchers) { + this.linkMatchers = matchers; + return this; + } + + void assertSpan(DDSpan span, DDSpan previousSpan) { // Apply parent id matcher from the previous span if (this.parentIdMatcher == CHILD_OF_PREVIOUS_MATCHER) { this.parentIdMatcher = is(previousSpan.getSpanId()); @@ -109,19 +149,27 @@ public void assertSpan(DDSpan span, DDSpan previousSpan) { assertValue(this.serviceNameMatcher, span.getServiceName(), "Expected service name"); assertValue(this.operationNameMatcher, span.getOperationName(), "Expected operation name"); assertValue(this.resourceNameMatcher, span.getResourceName(), "Expected resource name"); - assertValue( - this.durationMatcher, Duration.of(span.getDurationNano(), NANOS), "Expected duration"); - // TODO Add more values to test (tags, links, ...) - } - - private void assertValue(Matcher matcher, T value, String message) { - if (matcher != null && !matcher.test(value)) { - Optional expected = matcher.expected(); - if (expected.isPresent()) { - throw new AssertionFailedError(message + ". " + matcher.message(), expected.get(), value); - } else { - throw new AssertionFailedError(message + ": " + value + ". " + matcher.message()); - } + assertValue(this.durationMatcher, ofNanos(span.getDurationNano()), "Expected duration"); + assertValue(this.typeMatcher, span.getSpanType(), "Expected span type"); + assertValue(this.errorMatcher, span.isError(), "Expected error status"); + assertSpanTags(span.getTags()); + assertSpanLinks(spanLinks(span)); + } + + private void assertSpanTags(TagMap tags) { + // TODO Implement span tag assertions + } + + private void assertSpanLinks(List links) { + int linkCount = links == null ? 0 : links.size(); + int expectedLinkCount = this.linkMatchers == null ? 0 : this.linkMatchers.length; + if (linkCount != expectedLinkCount) { + throw new AssertionFailedError("Unexpected span link count", expectedLinkCount, linkCount); + } + for (int i = 0; i < expectedLinkCount; i++) { + SpanLinkMatcher linkMatcher = this.linkMatchers[expectedLinkCount]; + AgentSpanLink link = links.get(i); + linkMatcher.assertLink(link); } } } diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/core/DDSpanAccessor.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/core/DDSpanAccessor.java new file mode 100644 index 00000000000..3e88be78e4d --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/core/DDSpanAccessor.java @@ -0,0 +1,16 @@ +package datadog.trace.core; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import java.util.List; + +/** + * This class is a helper class to get access to package private span data that should not be + * exposed as part of the public API. + */ +public final class DDSpanAccessor { + private DDSpanAccessor() {} + + public static List spanLinks(DDSpan span) { + return span.links; + } +} diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java index b0c060b9df6..8627b215285 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationTest.java @@ -135,17 +135,17 @@ public void run() { assertTraces( trace( SORT_BY_START_TIME, - span().isRoot().withOperationName("parent"), - span().childOfPrevious().withOperationName("child"), - span().childOfPrevious().withOperationName("great-child"), - span().childOfPrevious().withOperationName("great-great-child"))); + span().isRoot().operationName("parent"), + span().childOfPrevious().operationName("child"), + span().childOfPrevious().operationName("great-child"), + span().childOfPrevious().operationName("great-great-child"))); } /** Verifies the parent / child span relation. */ void assertConnectedTrace() { assertTraces( trace( - span().isRoot().withOperationName("parent"), - span().childOfPrevious().withOperationName("asyncChild"))); + span().isRoot().operationName("parent"), + span().childOfPrevious().operationName("asyncChild"))); } } From dccd70aa3660cf390bdfade07fb9ca42759738ed Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 23 Dec 2025 15:41:52 +0100 Subject: [PATCH 3/3] feat(test): Add instrumentation test for JUnit --- .../test/AbstractInstrumentationTest.java | 4 +- .../trace/agent/test/assertions/Matchers.java | 3 +- .../agent/test/assertions/SpanMatcher.java | 47 +++++- .../agent/test/assertions/TagsMatcher.java | 134 ++++++++++++++++++ 4 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java index c7c79d045b1..d0b4e5b1b10 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java @@ -112,7 +112,7 @@ public void tearDown() { } /** - * Checks the structure of the traces captured from the test agent. + * Checks the structure of the traces captured from the test tracer. * * @param matchers The matchers to verify the trace collection, one matcher by expected trace. */ @@ -121,7 +121,7 @@ protected void assertTraces(TraceMatcher... matchers) { } /** - * Checks the structure of the traces captured from the test agent. + * Checks the structure of the traces captured from the test tracer. * * @param options The {@link TraceAssertions.Options} to configure the checks. * @param matchers The matchers to verify the trace collection, one matcher by expected trace. diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java index f2a98104595..f484d530b0c 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java @@ -10,6 +10,7 @@ * - introduce as few as possible matchers * - only have matchers for generic purpose, don't introduce feature / produce / use-case specific matchers * - name "ignores" as "any"? + * - think about extensibility? Open matchers for inheritance */ public class Matchers { @@ -21,7 +22,7 @@ public static Matcher isNull() { return new IsNull<>(); } - public static Matcher nonNull() { + public static Matcher isNonNull() { return new IsNonNull<>(); } diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java index e73d321cd94..6fc734afddd 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java @@ -3,10 +3,10 @@ import static datadog.trace.agent.test.assertions.Matchers.assertValue; import static datadog.trace.agent.test.assertions.Matchers.is; import static datadog.trace.agent.test.assertions.Matchers.isFalse; +import static datadog.trace.agent.test.assertions.Matchers.isNonNull; import static datadog.trace.agent.test.assertions.Matchers.isNull; import static datadog.trace.agent.test.assertions.Matchers.isTrue; import static datadog.trace.agent.test.assertions.Matchers.matches; -import static datadog.trace.agent.test.assertions.Matchers.nonNull; import static datadog.trace.agent.test.assertions.Matchers.validates; import static datadog.trace.core.DDSpanAccessor.spanLinks; import static java.time.Duration.ofNanos; @@ -15,7 +15,11 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpan; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Predicate; import java.util.regex.Pattern; import org.opentest4j.AssertionFailedError; @@ -35,6 +39,7 @@ public final class SpanMatcher { private Matcher durationMatcher; private Matcher typeMatcher; private Matcher errorMatcher; + private TagsMatcher[] tagMatchers; private SpanLinkMatcher[] linkMatchers; private static final Matcher CHILD_OF_PREVIOUS_MATCHER = is(0L); @@ -69,7 +74,7 @@ public SpanMatcher childOfPrevious() { } public SpanMatcher hasServiceName() { - this.serviceNameMatcher = nonNull(); + this.serviceNameMatcher = isNonNull(); return this; } @@ -133,6 +138,11 @@ public SpanMatcher withoutError() { return this; } + public SpanMatcher tags(TagsMatcher... matchers) { + this.tagMatchers = matchers; + return this; + } + public SpanMatcher links(SpanLinkMatcher... matchers) { this.linkMatchers = matchers; return this; @@ -157,7 +167,38 @@ void assertSpan(DDSpan span, DDSpan previousSpan) { } private void assertSpanTags(TagMap tags) { - // TODO Implement span tag assertions + // Check if tags should be asserted at all + if (this.tagMatchers == null) { + return; + } + // Collect all matchers + Map> matchers = new HashMap<>(); + for (TagsMatcher tagMatcher : this.tagMatchers) { + matchers.putAll(tagMatcher.tagMatchers); + } + // Assert all tags + List uncheckedTagNames = new ArrayList<>(); + tags.forEach( + (key, value) -> { + Matcher matcher = (Matcher) matchers.remove(key); + if (matcher == null) { + uncheckedTagNames.add(key); + } else { + assertValue(matcher, value, "Unexpected " + key + " tag value."); + } + }); + // Remove matchers that accept missing tags + Collection> values = matchers.values(); + values.removeIf(matcher -> matcher instanceof Any); + values.removeIf(matcher -> matcher instanceof IsNull); + // Fails if any tags are missing + if (!matchers.isEmpty()) { + throw new AssertionFailedError("Missing tags: " + String.join(", ", matchers.keySet())); + } + // Fails if any unexpected tags are present + if (!uncheckedTagNames.isEmpty()) { + throw new AssertionFailedError("Unexpected tags: " + String.join(", ", uncheckedTagNames)); + } } private void assertSpanLinks(List links) { diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java new file mode 100644 index 00000000000..21dae20ebaa --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java @@ -0,0 +1,134 @@ +package datadog.trace.agent.test.assertions; + +import static datadog.trace.agent.test.assertions.Matchers.any; +import static datadog.trace.agent.test.assertions.Matchers.is; +import static datadog.trace.agent.test.assertions.Matchers.isNonNull; +import static datadog.trace.api.DDTags.ERROR_MSG; +import static datadog.trace.api.DDTags.ERROR_STACK; +import static datadog.trace.api.DDTags.ERROR_TYPE; +import static datadog.trace.api.DDTags.LANGUAGE_TAG_KEY; +import static datadog.trace.api.DDTags.REQUIRED_CODE_ORIGIN_TAGS; +import static datadog.trace.api.DDTags.RUNTIME_ID_TAG; +import static datadog.trace.api.DDTags.THREAD_ID; +import static datadog.trace.api.DDTags.THREAD_NAME; +import static datadog.trace.common.sampling.RateByServiceTraceSampler.SAMPLING_AGENT_RATE; +import static datadog.trace.common.writer.ddagent.TraceMapper.SAMPLING_PRIORITY_KEY; + +import datadog.trace.api.DDTags; +import java.util.HashMap; +import java.util.Map; + +public final class TagsMatcher { + final Map> tagMatchers; + + private TagsMatcher(Map> tagMatchers) { + this.tagMatchers = tagMatchers; + } + + public static TagsMatcher defaultTags() { + Map> tagMatchers = new HashMap<>(); + tagMatchers.put(THREAD_NAME, isNonNull()); + tagMatchers.put(THREAD_ID, isNonNull()); + tagMatchers.put(RUNTIME_ID_TAG, any()); + tagMatchers.put(LANGUAGE_TAG_KEY, any()); + tagMatchers.put(SAMPLING_AGENT_RATE, any()); + tagMatchers.put(SAMPLING_PRIORITY_KEY.toString(), any()); + tagMatchers.put("_sample_rate", any()); + tagMatchers.put(DDTags.PID_TAG, any()); + tagMatchers.put(DDTags.SCHEMA_VERSION_TAG_KEY, any()); + tagMatchers.put(DDTags.PROFILING_ENABLED, any()); + tagMatchers.put(DDTags.PROFILING_CONTEXT_ENGINE, any()); + tagMatchers.put(DDTags.BASE_SERVICE, any()); + tagMatchers.put(DDTags.DSM_ENABLED, any()); + tagMatchers.put(DDTags.DJM_ENABLED, any()); + tagMatchers.put(DDTags.PARENT_ID, any()); + tagMatchers.put(DDTags.SPAN_LINKS, any()); // this is checked by LinksAsserter + + for (String tagName : REQUIRED_CODE_ORIGIN_TAGS) { + tagMatchers.put(tagName, any()); + } + // TODO Keep porting default tag logic + // TODO Dev notes: + // - it seems there is way too many logic there + // - need to check if its related to tracing only + + return new TagsMatcher(tagMatchers); + } + + /** + * Requires the following tag to match the given matcher. + * + * @param tagName The tag name to match. + * @param matcher The matcher to apply to the tag value. + * @return A tag matcher that requires the following tag to match the given matcher. + */ + public static TagsMatcher tag(String tagName, Matcher matcher) { + Map> tagMatchers = new HashMap<>(); + tagMatchers.put(tagName, matcher); + return new TagsMatcher(tagMatchers); + } + + /** + * Requires the following tags to be present. + * + * @param tagNames The tag names to match. + * @return A tag matcher that requires the following tags to be present. + */ + public static TagsMatcher includes(String... tagNames) { + Map> tagMatchers = new HashMap<>(); + for (String tagName : tagNames) { + tagMatchers.put(tagName, any()); + } + return new TagsMatcher(tagMatchers); + } + + /** + * Requires the error tags to match the given error. + * + * @param error The error to match. + * @return A tag matcher that requires the error tags to match the given error. + */ + public static TagsMatcher error(Throwable error) { + return error(error.getClass(), error.getMessage()); + } + + /** + * Requires the error tags to match the given error type. + * + * @param errorType The error type to match. + * @return A tag matcher that requires the error tags to match the given error type. + */ + public static TagsMatcher error(Class errorType) { + return error(errorType, null); + } + + /** + * Requires the error tags to match the given error type and message. + * + * @param errorType The error type to match. + * @param message The error message to match. + * @return A tag matcher that requires the error tags to match the given error type and message. + */ + public static TagsMatcher error(Class errorType, String message) { + Map> tagMatchers = new HashMap<>(); + tagMatchers.put(ERROR_TYPE, Matchers.validates(s -> testErrorType(errorType, s))); + tagMatchers.put(ERROR_STACK, isNonNull()); + if (message != null) { + tagMatchers.put(ERROR_MSG, is(message)); + } + return new TagsMatcher(tagMatchers); + } + + static boolean testErrorType(Class errorType, String actual) { + if (errorType.getName().equals(actual)) { + return true; + } + try { + // also accept type names which are subclasses of the given error type + return errorType.isAssignableFrom( + Class.forName(actual, false, TagsMatcher.class.getClassLoader())); + } catch (Throwable ignore) { + return false; + } + } +}