From 055b415d2094dc8516ec70fb0252caa2d5a4a99e Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 19 Dec 2025 11:10:50 +0100 Subject: [PATCH 1/2] DRAFT Groovy -> Java --- .../spymemcached/SpymemcachedTest.groovy | 1 - .../agent/test/asserts/LinksAssert.groovy | 58 --- .../test/asserts/ListWriterAssert.groovy | 172 ------- .../agent/test/asserts/SpanAssert.groovy | 211 --------- .../agent/test/asserts/TagsAssert.groovy | 277 ----------- .../agent/test/asserts/TraceAssert.groovy | 90 ---- .../trace/agent/test/asserts/LinksAssert.java | 111 +++++ .../agent/test/asserts/ListWriterAssert.java | 194 ++++++++ .../trace/agent/test/asserts/SpanAssert.java | 268 +++++++++++ .../trace/agent/test/asserts/TagsAssert.java | 435 ++++++++++++++++++ .../trace/agent/test/asserts/TraceAssert.java | 98 ++++ 11 files changed, 1106 insertions(+), 809 deletions(-) delete mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/LinksAssert.groovy delete mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/ListWriterAssert.groovy delete mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy delete mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy delete mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TraceAssert.groovy create mode 100644 dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/LinksAssert.java create mode 100644 dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java create mode 100644 dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java create mode 100644 dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java create mode 100644 dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java diff --git a/dd-java-agent/instrumentation/spymemcached-2.10/src/test/groovy/datadog/trace/instrumentation/spymemcached/SpymemcachedTest.groovy b/dd-java-agent/instrumentation/spymemcached-2.10/src/test/groovy/datadog/trace/instrumentation/spymemcached/SpymemcachedTest.groovy index f8d8d3d593d..7a285175a30 100644 --- a/dd-java-agent/instrumentation/spymemcached-2.10/src/test/groovy/datadog/trace/instrumentation/spymemcached/SpymemcachedTest.groovy +++ b/dd-java-agent/instrumentation/spymemcached-2.10/src/test/groovy/datadog/trace/instrumentation/spymemcached/SpymemcachedTest.groovy @@ -26,7 +26,6 @@ import java.util.concurrent.locks.ReentrantLock import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE import static datadog.trace.instrumentation.spymemcached.MemcacheClientDecorator.* -import static datadog.trace.instrumentation.spymemcached.MemcacheClientDecorator.COMPONENT_NAME import static net.spy.memcached.ConnectionFactoryBuilder.Protocol.BINARY abstract class SpymemcachedTest extends VersionedNamingTestBase { diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/LinksAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/LinksAssert.groovy deleted file mode 100644 index 3d583398cac..00000000000 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/LinksAssert.groovy +++ /dev/null @@ -1,58 +0,0 @@ -package datadog.trace.agent.test.asserts - -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.bootstrap.instrumentation.api.SpanLink -import datadog.trace.core.DDSpan -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType - -class LinksAssert { - private final List links - private final Set assertedLinks = [] - - private LinksAssert(DDSpan span) { - this.links = span.links // this is class protected but for the moment groovy can access it - } - - static void assertLinks(DDSpan span, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.LinksAssert']) - @DelegatesTo(value = LinksAssert, strategy = Closure.DELEGATE_FIRST) Closure spec, - boolean checkAllLinks = true) { - def asserter = new LinksAssert(span) - def clone = (Closure) spec.clone() - clone.delegate = asserter - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone(asserter) - if (checkAllLinks) { - asserter.assertLinksAllVerified() - } - } - - def link(DDSpan linked, byte flags = SpanLink.DEFAULT_FLAGS, SpanAttributes attributes = SpanAttributes.EMPTY, String traceState = '') { - link(linked.context(), flags, attributes, traceState) - } - - def link(AgentSpanContext context, byte flags = SpanLink.DEFAULT_FLAGS, SpanAttributes attributes = SpanAttributes.EMPTY, String traceState = '') { - link(context.traceId, context.spanId, flags, attributes, traceState) - } - - def link(DDTraceId traceId, long spanId, byte flags = SpanLink.DEFAULT_FLAGS, SpanAttributes attributes = SpanAttributes.EMPTY, String traceState = '') { - def found = links.find { - it.spanId() == spanId && it.traceId() == traceId - } - assert found != null - assert found.traceFlags() == flags - assert found.attributes() == attributes - assert found.traceState() == traceState - assertedLinks.add(found) - } - - void assertLinksAllVerified() { - def list = new ArrayList(links) - list.removeAll(assertedLinks) - assert list.isEmpty() - } -} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/ListWriterAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/ListWriterAssert.groovy deleted file mode 100644 index 166eb92f6b6..00000000000 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/ListWriterAssert.groovy +++ /dev/null @@ -1,172 +0,0 @@ -package datadog.trace.agent.test.asserts - -import static TraceAssert.assertTrace - -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.DDSpan -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType -import org.codehaus.groovy.runtime.powerassert.PowerAssertionError -import org.spockframework.runtime.Condition -import org.spockframework.runtime.ConditionNotSatisfiedError -import org.spockframework.runtime.model.TextPosition - -import java.util.concurrent.atomic.AtomicInteger - -class ListWriterAssert { - public static final Comparator> SORT_TRACES_BY_ID = new SortTracesById() - public static final Comparator> SORT_TRACES_BY_START = new SortTracesByStart() - public static final Comparator> SORT_TRACES_BY_NAMES = new SortTracesByNames() - - private List> traces - private final int size - private final Set assertedIndexes = new HashSet<>() - private final AtomicInteger traceAssertCount = new AtomicInteger(0) - - private ListWriterAssert(List> traces) { - this.traces = traces - size = traces.size() - } - - static void assertTraces(ListWriter writer, int expectedSize, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.ListWriterAssert']) - @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - assertTraces(writer, expectedSize, false, spec) - } - - static void assertTraces(ListWriter writer, int expectedSize, - boolean ignoreAdditionalTraces, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.ListWriterAssert']) - @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - assertTraces(writer, expectedSize, ignoreAdditionalTraces, SORT_TRACES_BY_START, spec) - } - - static void assertTraces(ListWriter writer, int expectedSize, - boolean ignoreAdditionalTraces, - Comparator> traceSorter, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.ListWriterAssert']) - @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - try { - writer.waitForTraces(expectedSize) - def array = writer.toArray() - assert array.length == expectedSize - def traces = (Arrays.asList(array) as List>) - Collections.sort(traces, traceSorter) - def asserter = new ListWriterAssert(traces) - def clone = (Closure) spec.clone() - clone.delegate = asserter - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone(asserter) - if (!ignoreAdditionalTraces) { - asserter.assertTracesAllVerified() - if (writer.size() > traces.size()) { - def extras = new ArrayList<>(writer) - extras.removeAll(traces) - def message = new StringBuilder("ListWriter obtained ${extras.size()} additional traces while validating:") - extras.each { - message.append('\n') - message.append(it) - } - message.append('\n') - throw new AssertionError(message) - } - } - } catch (PowerAssertionError e) { - def stackLine = null - for (int i = 0; i < e.stackTrace.length; i++) { - def className = e.stackTrace[i].className - def skip = className.startsWith("org.codehaus.groovy.") || - className.startsWith("datadog.trace.agent.test.") || - className.startsWith("sun.reflect.") || - className.startsWith("groovy.lang.") || - className.startsWith("java.lang.") - if (skip) { - continue - } - stackLine = e.stackTrace[i] - break - } - def condition = new Condition(null, "$stackLine", TextPosition.create(stackLine == null ? 0 : stackLine.lineNumber, 0), e.message, null, e) - throw new ConditionNotSatisfiedError(condition, e) - } - } - - void sortSpansByStart() { - traces = traces.collect { - return new ArrayList(it).sort { a, b -> - return a.startTimeNano <=> b.startTimeNano - } - } - } - - List trace(int index) { - return Collections.unmodifiableList(traces.get(index)) - } - - void trace(int expectedSize, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TraceAssert']) - @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - trace(expectedSize, false, spec) - } - void trace(int expectedSize, boolean sortByName, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TraceAssert']) - @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - trace(expectedSize, sortByName ? TraceAssert.NAME_COMPARATOR : null, spec) - } - - void trace(int expectedSize, Comparator sorter, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TraceAssert']) - @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - def index = traceAssertCount.getAndIncrement() - - if (index >= size) { - throw new ArrayIndexOutOfBoundsException(index) - } - if (traces.size() != size) { - throw new ConcurrentModificationException("ListWriter modified during assertion") - } - assertedIndexes.add(index) - assertTrace(trace(index), expectedSize, sorter, spec) - } - - void assertTracesAllVerified() { - assert assertedIndexes.size() == size - } - - private static class SortTracesByStart implements Comparator> { - @Override - int compare(List o1, List o2) { - return Long.compare(traceStart(o1), traceStart(o2)) - } - - long traceStart(List trace) { - assert !trace.isEmpty() - return trace.get(0).localRootSpan.startTime - } - } - - private static class SortTracesById implements Comparator> { - @Override - int compare(List o1, List o2) { - return Long.compare(rootSpanId(o1), rootSpanId(o2)) - } - - long rootSpanId(List trace) { - assert !trace.isEmpty() - return trace.get(0).localRootSpan.spanId.toLong() - } - } - - private static class SortTracesByNames implements Comparator> { - @Override - int compare(List o1, List o2) { - return rootSpanTrace(o1) <=> rootSpanTrace(o2) - } - - String rootSpanTrace(List trace) { - assert !trace.isEmpty() - def rootSpan = trace.get(0).localRootSpan - return "${rootSpan.serviceName}/${rootSpan.operationName}/${rootSpan.resourceName}" - } - } -} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy deleted file mode 100644 index 92097c2c96a..00000000000 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy +++ /dev/null @@ -1,211 +0,0 @@ -package datadog.trace.agent.test.asserts - -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import datadog.trace.core.DDSpan -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType - -import java.util.regex.Pattern - -import static datadog.trace.agent.test.asserts.LinksAssert.assertLinks -import static datadog.trace.agent.test.asserts.TagsAssert.assertTags - -class SpanAssert { - private final DDSpan span - private final DDSpan previous - private boolean checkLinks = true - private final checked = [:] - - private SpanAssert(span, DDSpan previous) { - this.span = span - this.previous = previous - } - - static void assertSpan(DDSpan span, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.SpanAssert']) - @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec, - DDSpan previous = null) { - def asserter = new SpanAssert(span, previous) - asserter.assertSpan spec - } - - void assertSpan( - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.SpanAssert']) - @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - def clone = (Closure) spec.clone() - clone.delegate = this - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone(this) - assertDefaults() - } - - def assertSpanNameContains(String spanName, String... shouldContainArr) { - for (String shouldContain : shouldContainArr) { - assert spanName.toString().contains(shouldContain) - } - } - - def hasServiceName() { - assert span.serviceName != null && !span.serviceName.isEmpty() - } - - def serviceName(String name) { - assert span.serviceName == name - checked.serviceName = true - } - - def operationName(String name) { - assert span.operationName.toString() == name - checked.operationName = true - } - - def operationName(Closure eval) { - assert eval(span.operationName.toString()) - checked.resourceName = true - } - - def operationNameContains(String... operationNameParts) { - assertSpanNameContains(span.operationName.toString(), operationNameParts) - checked.operationName = true - } - - def resourceName(Pattern pattern) { - assert span.resourceName.toString().matches(pattern) - checked.resourceName = true - } - - def resourceName(String name) { - assert span.resourceName.toString() == name - checked.resourceName = true - } - - def resourceName(Closure eval) { - assert eval(span.resourceName.toString()) - checked.resourceName = true - } - - def resourceNameContains(String... resourceNameParts) { - assertSpanNameContains(span.resourceName.toString(), resourceNameParts) - checked.resourceName = true - } - - def duration(Closure eval) { - assert eval(span.durationNano) - checked.duration = true - } - - def spanType(String type) { - if (null == span.spanType) { - // code less readable makes for a better assertion message, don't want NPE - assert span.spanType == type - } else { - assert span.spanType.toString() == type - } - assert span.tags["span.type"] == null - checked.spanType = true - } - - def parent() { - assert span.parentId == DDSpanId.ZERO - checked.parentId = true - } - - def parentSpanId(BigInteger parentId) { - long id = parentId == null ? 0 : DDSpanId.from("$parentId") - assert span.parentId == id - checked.parentId = true - } - - def traceId(BigInteger traceId) { - traceDDId(traceId != null ? DDTraceId.from("$traceId") : null) - } - - def traceDDId(DDTraceId traceId) { - assert span.traceId == traceId - checked.traceId = true - } - - def childOf(DDSpan parent) { - assert span.parentId == parent.spanId - checked.parentId = true - assert span.traceId == parent.traceId - checked.traceId = true - } - - def childOfPrevious() { - assert previous != null - childOf(previous) - } - - def threadNameStartsWith(String threadName) { - assert span.tags.get("thread.name")?.startsWith(threadName) - } - - def notChildOf(DDSpan parent) { - assert parent.spanId != span.parentId - assert parent.traceId != span.traceId - } - - def errored(boolean errored) { - assert span.isError() == errored - checked.errored = true - } - - def topLevel(boolean topLevel) { - assert span.isTopLevel() == topLevel - checked.topLevel = true - } - - def measured(boolean measured) { - assert span.measured == measured - checked.measured = true - } - - void assertDefaults() { - if (!checked.spanType) { - spanType(null) - } - if (!checked.errored) { - errored(false) - } - if (checkLinks) { - if (!checked.links) { - assert span.tags['_dd.span_links'] == null - } - } - hasServiceName() - } - - void tags(@ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TagsAssert']) - @DelegatesTo(value = TagsAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - assertTags(span, spec) - } - - void tags(boolean checkAllTags, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TagsAssert']) - @DelegatesTo(value = TagsAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - assertTags(span, spec, checkAllTags) - } - - void ignoreSpanLinks() { - this.checkLinks = false - } - - void links(@ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.LinksAssert']) - @DelegatesTo(value = LinksAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - checked.links = true - assertLinks(span, spec) - } - - void links(boolean checkAllLinks, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.LinksAssert']) - @DelegatesTo(value = LinksAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - checked.links = true - assertLinks(span, spec, checkAllLinks) - } - - DDSpan getSpan() { - return span - } -} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy deleted file mode 100644 index ba3e5d6a397..00000000000 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy +++ /dev/null @@ -1,277 +0,0 @@ -package datadog.trace.agent.test.asserts - -import datadog.trace.api.Config -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTags -import datadog.trace.api.naming.SpanNaming -import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.common.sampling.RateByServiceTraceSampler -import datadog.trace.common.writer.ListWriter -import datadog.trace.common.writer.ddagent.TraceMapper -import datadog.trace.core.DDSpan -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType - -import java.util.regex.Pattern - -class TagsAssert { - private final long spanParentId - private final Map tags - private final Set assertedTags = new TreeSet<>() - - private TagsAssert(DDSpan span) { - this.spanParentId = span.parentId - this.tags = span.tags - } - - static void assertTags(DDSpan span, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TagsAssert']) - @DelegatesTo(value = TagsAssert, strategy = Closure.DELEGATE_FIRST) Closure spec, - boolean checkAllTags = true) { - def asserter = new TagsAssert(span) - def clone = (Closure) spec.clone() - clone.delegate = asserter - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone(asserter) - if (checkAllTags) { - asserter.assertTagsAllVerified() - } - } - - /** - * Check that, if the peer.service tag source has been set, it matches the provided one. - * @param sourceTag the source to match - */ - def peerServiceFrom(String sourceTag) { - tag(DDTags.PEER_SERVICE_SOURCE, { SpanNaming.instance().namingSchema().peerService().supports() ? it == sourceTag : it == null }) - } - - def withCustomIntegrationName(String integrationName) { - assertedTags.add(DDTags.DD_INTEGRATION) - assert tags[DDTags.DD_INTEGRATION]?.toString() == integrationName - } - - def defaultTagsNoPeerService(boolean distributedRootSpan = false) { - defaultTags(distributedRootSpan, false) - } - - def isPresent(String name) { - tag(name, { it != null }) - } - - def arePresent(Collection tags) { - for (String name : tags) { - isPresent(name) - } - } - - def isNotPresent(String name) { - tag(name, { it == null }) - } - - def areNotPresent(Collection tags) { - for (String name : tags) { - isNotPresent(name) - } - } - - /** - * @param distributedRootSpan set to true if current span has a parent span but still considered 'root' for current service - */ - def defaultTags(boolean distributedRootSpan = false, boolean checkPeerService = true) { - assertedTags.add("thread.name") - assertedTags.add("thread.id") - assertedTags.add(DDTags.RUNTIME_ID_TAG) - assertedTags.add(DDTags.LANGUAGE_TAG_KEY) - assertedTags.add(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) - assertedTags.add(TraceMapper.SAMPLING_PRIORITY_KEY.toString()) - assertedTags.add("_sample_rate") - assertedTags.add(DDTags.PID_TAG) - assertedTags.add(DDTags.SCHEMA_VERSION_TAG_KEY) - assertedTags.add(DDTags.PROFILING_ENABLED) - assertedTags.add(DDTags.PROFILING_CONTEXT_ENGINE) - assertedTags.add(DDTags.BASE_SERVICE) - assertedTags.add(DDTags.DSM_ENABLED) - assertedTags.add(DDTags.DJM_ENABLED) - assertedTags.add(DDTags.PARENT_ID) - assertedTags.add(DDTags.SPAN_LINKS) // this is checked by LinksAsserter - DDTags.REQUIRED_CODE_ORIGIN_TAGS.each { - assertedTags.add(it) - } - if (assertedTags.add(DDTags.DD_INTEGRATION) && tags[Tags.COMPONENT] != null) { - assert tags[Tags.COMPONENT].toString() == tags[DDTags.DD_INTEGRATION].toString() - } - - assert tags["thread.name"] != null - assert tags["thread.id"] != null - - // FIXME: DQH - Too much conditional logic? Maybe create specialized methods for client & server cases - - boolean isRoot = (DDSpanId.ZERO == spanParentId) - if (isRoot) { - assert tags[DDTags.SCHEMA_VERSION_TAG_KEY] == SpanNaming.instance().version() - } - if (isRoot || distributedRootSpan) { - // If runtime id is actually different here, it might indicate that - // the Config class was loaded on multiple different class loaders. - assert tags[DDTags.RUNTIME_ID_TAG] == Config.get().runtimeId - assertedTags.add(DDTags.TRACER_HOST) - assert tags[DDTags.TRACER_HOST] == Config.get().getHostName() - } else { - assert tags[DDTags.RUNTIME_ID_TAG] == null - } - String spanKind = tags[Tags.SPAN_KIND] - boolean isServer = (spanKind == Tags.SPAN_KIND_SERVER) - if (isRoot || distributedRootSpan || isServer) { - assert tags[DDTags.LANGUAGE_TAG_KEY] == DDTags.LANGUAGE_TAG_VALUE - } else { - assert tags[DDTags.LANGUAGE_TAG_KEY] == null - } - boolean shouldSetPeerService = checkPeerService && (spanKind == Tags.SPAN_KIND_CLIENT || spanKind == Tags.SPAN_KIND_PRODUCER) - if (shouldSetPeerService && SpanNaming.instance().namingSchema().peerService().supports()) { - assertedTags.add(Tags.PEER_SERVICE) - assertedTags.add(DDTags.PEER_SERVICE_SOURCE) - assert tags[Tags.PEER_SERVICE] != null - assert tags[Tags.PEER_SERVICE] == tags[tags[DDTags.PEER_SERVICE_SOURCE]] - } else { - assert tags[Tags.PEER_SERVICE] == null - assert tags[DDTags.PEER_SERVICE_SOURCE] == null - } - } - - static void codeOriginTags(ListWriter writer) { - if (Config.get().isDebuggerCodeOriginEnabled()) { - def traces = new ArrayList<>(writer) //as List> - - def spans = [] - traces.each { - it.each { - if (it.tags[DDTags.DD_CODE_ORIGIN_TYPE] != null) { - spans += it - } - } - } - assert !spans.isEmpty(): "Should have found at least one span with code origin" - spans.each { - assertTags(it, { - DDTags.REQUIRED_CODE_ORIGIN_TAGS.each { - assert tags[it] != null: "Should have found ${it} in span tags: " + tags.keySet() - } - }, false) - } - } - } - - def errorTags(Throwable error) { - errorTags(error.getClass(), error.getMessage()) - } - - def errorTags(Class errorType) { - errorTags(errorType, null) - } - - def errorTags(Class errorType, message) { - tag("error.type", { - if (it == errorType.name) { - return true - } - try { - // also accept type names which are sub-classes of the given error type - return errorType.isAssignableFrom( - Class.forName(it as String, false, getClass().getClassLoader())) - } catch (Throwable ignore) { - return false - } - }) - tag("error.stack", String) - - if (message != null) { - tag("error.message", message) - } - } - - def urlTags(String url, List queryParams){ - tag("http.url", { - URI uri = new URI(it.toString().split("\\?", 2)[0]) - String scheme = uri.getScheme() - String host = uri.getHost() - int port = uri.getPort() - String path = uri.getPath() - - String baseURL = scheme + "://" + host + ":" + port + path - return baseURL.equals(url) - }) - - tag("http.query.string", { - String paramString = it - System.out.println("it: " + it) - Set spanQueryParams = new HashSet() - if (paramString != null && !paramString.isEmpty()) { - String[] pairs = paramString.split("&") - for (String pair : pairs) { - int idx = pair.indexOf('=') - if (idx > 0) { - spanQueryParams.add(pair.substring(0, idx)) - } else { - spanQueryParams.add(pair) - } - } - for(String param : queryParams){ - if (!spanQueryParams.contains(param)){ - System.out.println("param: " + param) - return false - } - } - } else if(queryParams != null && queryParams.size() > 0){ - //if http.query.string is empty/null but we expect queryParams, return false - return false - } - return true - }) - } - - def tag(String name, expected) { - if (expected == null) { - return - } - assertedTags.add(name) - def value = tag(name) - if (expected instanceof Pattern) { - assert value =~ expected: "Tag \"$name\": \"${value.toString()}\" does not match pattern \"$expected\"" - } else if (expected instanceof Class) { - assert ((Class) expected).isInstance(value): "Tag \"$name\": instance check $expected failed for \"${value.toString()}\" of class \"${value.class}\"" - } else if (expected instanceof Closure) { - assert ((Closure) expected).call(value): "Tag \"$name\": closure call ${expected.toString()} failed with \"$value\"" - } else if (expected instanceof CharSequence) { - assert value == expected.toString(): "Tag \"$name\": \"$value\" != \"${expected.toString()}\"" - } else { - assert value == expected: "Tag \"$name\": \"$value\" != \"$expected\"" - } - } - - def tag(String name) { - def t = tags[name] - return (t instanceof CharSequence) ? t.toString() : t - } - - def methodMissing(String name, args) { - if (args.length == 0) { - throw new IllegalArgumentException(args.toString()) - } - tag(name, args[0]) - } - - def addTags(Map tags) { - tags.each { tag(it.key, it.value) } - true - } - - void assertTagsAllVerified() { - def set = new TreeMap<>(tags).keySet() - set.removeAll(assertedTags) - // The primary goal is to ensure the set is empty. - // tags and assertedTags are included via an "always true" comparison - // so they provide better context in the error message. - assert tags.entrySet() != assertedTags && set.isEmpty() - } -} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TraceAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TraceAssert.groovy deleted file mode 100644 index ab67d516079..00000000000 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TraceAssert.groovy +++ /dev/null @@ -1,90 +0,0 @@ -package datadog.trace.agent.test.asserts - -import datadog.trace.core.DDSpan -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType - -import java.util.concurrent.atomic.AtomicInteger - -import static SpanAssert.assertSpan - -class TraceAssert { - private List trace - private final int size - private final Set assertedIndexes = new HashSet<>() - private final AtomicInteger spanAssertCount = new AtomicInteger(0) - - private TraceAssert(trace) { - this.trace = Collections.unmodifiableList(trace) - size = trace.size() - } - - static final NAME_COMPARATOR = new Comparator() { - @Override - int compare(DDSpan o1, DDSpan o2) { - int compare = o1.spanName.toString() <=> o2.spanName.toString() - return compare != 0 ? compare : String.valueOf(o1.resourceName) <=> String.valueOf(o2.resourceName) - } - } - - static void assertTrace(List trace, int expectedSize, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TraceAssert']) - @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - assertTrace(trace, expectedSize, null, spec) - } - - static void assertTrace(List trace, int expectedSize, Comparator sorter, - @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TraceAssert']) - @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - // Some tests do their own sorting of the spans which can happen concurrently with other code doing - // iterations, so we make a copy of the list here to not cause a ConcurrentModificationException - trace = new ArrayList(trace) - assert trace.size() == expectedSize - if (sorter != null) { - Collections.sort(trace, sorter) - } - def asserter = new TraceAssert(trace) - def clone = (Closure) spec.clone() - clone.delegate = asserter - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone(asserter) - asserter.assertSpansAllVerified() - } - - DDSpan span(int index) { - trace.get(index) - } - - int nextSpanId() { - return spanAssertCount.getAndIncrement() - } - - void span(@ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.SpanAssert']) @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - span(nextSpanId(), spec) - } - - void span(int index, @ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.SpanAssert']) @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { - if (index >= size) { - throw new ArrayIndexOutOfBoundsException(index) - } - if (trace.size() != size) { - throw new ConcurrentModificationException("Trace modified during assertion") - } - assertedIndexes.add(index) - if (index > 0) { - assertSpan(trace.get(index), spec, trace.get(index - 1)) - } else { - assertSpan(trace.get(index), spec) - } - } - - void assertSpansAllVerified() { - assert assertedIndexes.size() == size - } - - void sortSpansByStart() { - trace = Collections.unmodifiableList(new ArrayList(trace).sort { a, b -> - return a.startTimeNano <=> b.startTimeNano - }) - } -} diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/LinksAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/LinksAssert.java new file mode 100644 index 00000000000..f10bf66bebd --- /dev/null +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/LinksAssert.java @@ -0,0 +1,111 @@ +package datadog.trace.agent.test.asserts; + +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.bootstrap.instrumentation.api.SpanLink; +import datadog.trace.core.DDSpan; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +public class LinksAssert { + + private final List links; + private final Set assertedLinks = new HashSet<>(); + + private LinksAssert(DDSpan span) { + // In Groovy, this accesses a package/protected field; here we assume a getter exists or + // same-package access. + this.links = getLinksFromSpan(span); + } + + private static List getLinksFromSpan(DDSpan span) { + try { + Field linksField = span.getClass().getDeclaredField("links"); + linksField.setAccessible(true); + return (List) linksField.get(span); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public static void assertLinks(DDSpan span, Consumer spec, boolean checkAllLinks) { + + LinksAssert asserter = new LinksAssert(span); + spec.accept(asserter); + if (checkAllLinks) { + asserter.assertLinksAllVerified(); + } + } + + public static void assertLinks(DDSpan span, Consumer spec) { + + assertLinks(span, spec, true); + } + + public void link(DDSpan linked, byte flags, SpanAttributes attributes, String traceState) { + + link(linked.context(), flags, attributes, traceState); + } + + public void link(DDSpan linked) { + link(linked, SpanLink.DEFAULT_FLAGS, SpanAttributes.EMPTY, ""); + } + + public void link( + AgentSpanContext context, byte flags, SpanAttributes attributes, String traceState) { + + link(context.getTraceId(), context.getSpanId(), flags, attributes, traceState); + } + + public void link(AgentSpanContext context) { + link(context, SpanLink.DEFAULT_FLAGS, SpanAttributes.EMPTY, ""); + } + + public void link( + DDTraceId traceId, long spanId, byte flags, SpanAttributes attributes, String traceState) { + + AgentSpanLink found = null; + for (AgentSpanLink link : links) { + if (link.spanId() == spanId && link.traceId().equals(traceId)) { + found = link; + break; + } + } + + if (found == null) { + throw new AssertionError( + "Expected link for traceId=" + traceId + " spanId=" + spanId + " not found"); + } + if (found.traceFlags() != flags) { + throw new AssertionError("Expected traceFlags=" + flags + " but was " + found.traceFlags()); + } + if (!found.attributes().equals(attributes)) { + throw new AssertionError( + "Expected attributes=" + attributes + " but was " + found.attributes()); + } + if (!found.traceState().equals(traceState)) { + throw new AssertionError( + "Expected traceState=\"" + traceState + "\" but was \"" + found.traceState() + "\""); + } + + assertedLinks.add(found); + } + + public void link(DDTraceId traceId, long spanId) { + link(traceId, spanId, SpanLink.DEFAULT_FLAGS, SpanAttributes.EMPTY, ""); + } + + public void assertLinksAllVerified() { + List remaining = new ArrayList<>(links); + remaining.removeAll(assertedLinks); + if (!remaining.isEmpty()) { + throw new AssertionError("Not all links were verified. Remaining: " + remaining.size()); + } + } +} diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java new file mode 100644 index 00000000000..a5fd32ede10 --- /dev/null +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java @@ -0,0 +1,194 @@ +package datadog.trace.agent.test.asserts; + +import static datadog.trace.agent.test.asserts.TraceAssert.assertTrace; + +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.DDSpan; +import java.util.*; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.codehaus.groovy.runtime.powerassert.PowerAssertionError; +import org.spockframework.runtime.Condition; +import org.spockframework.runtime.ConditionNotSatisfiedError; +import org.spockframework.runtime.model.TextPosition; + +public class ListWriterAssert { + + public static final Comparator> SORT_TRACES_BY_ID = new SortTracesById(); + public static final Comparator> SORT_TRACES_BY_START = new SortTracesByStart(); + public static final Comparator> SORT_TRACES_BY_NAMES = new SortTracesByNames(); + + private List> traces; + private final int size; + private final Set assertedIndexes = new HashSet<>(); + private final AtomicInteger traceAssertCount = new AtomicInteger(0); + + private ListWriterAssert(List> traces) { + this.traces = traces; + this.size = traces.size(); + } + + public static void assertTraces( + ListWriter writer, int expectedSize, Consumer spec) { + assertTraces(writer, expectedSize, false, SORT_TRACES_BY_START, spec); + } + + public static void assertTraces( + ListWriter writer, + int expectedSize, + boolean ignoreAdditionalTraces, + Consumer spec) { + assertTraces(writer, expectedSize, ignoreAdditionalTraces, SORT_TRACES_BY_START, spec); + } + + public static void assertTraces( + ListWriter writer, + int expectedSize, + boolean ignoreAdditionalTraces, + Comparator> traceSorter, + Consumer spec) { + + try { + writer.waitForTraces(expectedSize); + Object[] array = writer.toArray(); + if (array.length != expectedSize) { + throw new AssertionError("Expected " + expectedSize + " traces but got " + array.length); + } + + List> traces = new ArrayList<>(); + for (Object o : array) { + traces.add((List) o); + } + + traces.sort(traceSorter); + ListWriterAssert asserter = new ListWriterAssert(traces); + spec.accept(asserter); + + if (!ignoreAdditionalTraces) { + asserter.assertTracesAllVerified(); + + if (writer.size() > traces.size()) { + List> extras = new ArrayList<>((Collection>) writer); + extras.removeAll(traces); + + StringBuilder message = + new StringBuilder("ListWriter obtained ") + .append(extras.size()) + .append(" additional traces while validating:"); + for (List extra : extras) { + message.append("\n").append(extra); + } + message.append('\n'); + throw new AssertionError(message.toString()); + } + } + } catch (PowerAssertionError e) { + StackTraceElement stackLine = null; + for (StackTraceElement element : e.getStackTrace()) { + String className = element.getClassName(); + boolean skip = + className.startsWith("org.codehaus.groovy.") + || className.startsWith("datadog.trace.agent.test.") + || className.startsWith("sun.reflect.") + || className.startsWith("groovy.lang.") + || className.startsWith("java.lang."); + if (!skip) { + stackLine = element; + break; + } + } + Condition condition = + new Condition( + null, + String.valueOf(stackLine), + TextPosition.create(stackLine == null ? 0 : stackLine.getLineNumber(), 0), + e.getMessage(), + null, + e); + throw new ConditionNotSatisfiedError(condition, e); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + public void sortSpansByStart() { + this.traces = + traces.stream() + .map( + it -> { + List copy = new ArrayList<>(it); + copy.sort(Comparator.comparingLong(span -> span.getStartTimeNano())); + return copy; + }) + .collect(Collectors.toList()); + } + + public List trace(int index) { + return Collections.unmodifiableList(traces.get(index)); + } + + public void trace(int expectedSize, Consumer spec) { + trace(expectedSize, false, spec); + } + + public void trace(int expectedSize, boolean sortByName, Consumer spec) { + trace(expectedSize, sortByName ? TraceAssert.NAME_COMPARATOR : null, spec); + } + + public void trace(int expectedSize, Comparator sorter, Consumer spec) { + int index = traceAssertCount.getAndIncrement(); + if (index >= size) { + throw new ArrayIndexOutOfBoundsException(index); + } + if (traces.size() != size) { + throw new ConcurrentModificationException("ListWriter modified during assertion"); + } + assertedIndexes.add(index); + assertTrace(trace(index), expectedSize, sorter, spec); + } + + public void assertTracesAllVerified() { + if (assertedIndexes.size() != size) { + throw new AssertionError("Not all traces were verified."); + } + } + + private static class SortTracesByStart implements Comparator> { + @Override + public int compare(List o1, List o2) { + return Long.compare(traceStart(o1), traceStart(o2)); + } + + private long traceStart(List trace) { + if (trace.isEmpty()) throw new AssertionError("Trace empty"); + return trace.get(0).getLocalRootSpan().getStartTime(); + } + } + + private static class SortTracesById implements Comparator> { + @Override + public int compare(List o1, List o2) { + return Long.compare(rootSpanId(o1), rootSpanId(o2)); + } + + private long rootSpanId(List trace) { + if (trace.isEmpty()) throw new AssertionError("Trace empty"); + return trace.get(0).getLocalRootSpan().getSpanId(); + } + } + + private static class SortTracesByNames implements Comparator> { + @Override + public int compare(List o1, List o2) { + return rootSpanTrace(o1).compareTo(rootSpanTrace(o2)); + } + + private String rootSpanTrace(List trace) { + if (trace.isEmpty()) throw new AssertionError("Trace empty"); + DDSpan root = trace.get(0).getLocalRootSpan(); + return root.getServiceName() + "/" + root.getOperationName() + "/" + root.getResourceName(); + } + } +} diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java new file mode 100644 index 00000000000..d1a683b9892 --- /dev/null +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java @@ -0,0 +1,268 @@ +package datadog.trace.agent.test.asserts; + +import static datadog.trace.agent.test.asserts.LinksAssert.assertLinks; +import static datadog.trace.agent.test.asserts.TagsAssert.assertTags; + +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; +import datadog.trace.core.DDSpan; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +public class SpanAssert { + + private final DDSpan span; + private final DDSpan previous; + private boolean checkLinks = true; + private final Map checked = new HashMap<>(); + + private SpanAssert(DDSpan span, DDSpan previous) { + this.span = span; + this.previous = previous; + } + + public static void assertSpan(DDSpan span, Consumer spec, DDSpan previous) { + + SpanAssert asserter = new SpanAssert(span, previous); + asserter.assertSpan(spec); + } + + public static void assertSpan(DDSpan span, Consumer spec) { + + assertSpan(span, spec, null); + } + + public void assertSpan(Consumer spec) { + spec.accept(this); + assertDefaults(); + } + + public void assertSpanNameContains(String spanName, String... shouldContainArr) { + for (String shouldContain : shouldContainArr) { + if (!spanName.contains(shouldContain)) { + throw new AssertionError( + "Span name \"" + spanName + "\" does not contain \"" + shouldContain + "\""); + } + } + } + + public void hasServiceName() { + if (span.getServiceName() == null || span.getServiceName().isEmpty()) { + throw new AssertionError("Service name is null or empty"); + } + } + + public void serviceName(String name) { + if (!name.equals(span.getServiceName())) { + throw new AssertionError( + "Expected serviceName \"" + name + "\" but was \"" + span.getServiceName() + "\""); + } + checked.put("serviceName", true); + } + + public void operationName(String name) { + String op = span.getOperationName().toString(); + if (!op.equals(name)) { + throw new AssertionError("Expected operationName \"" + name + "\" but was \"" + op + "\""); + } + checked.put("operationName", true); + } + + public void operationNameMatches(java.util.function.Predicate eval) { + String op = span.getOperationName().toString(); + if (!eval.test(op)) { + throw new AssertionError("operationName predicate did not match: " + op); + } + checked.put("operationName", true); + } + + public void operationNameContains(String... operationNameParts) { + assertSpanNameContains(span.getOperationName().toString(), operationNameParts); + checked.put("operationName", true); + } + + public void resourceName(Pattern pattern) { + String res = span.getResourceName().toString(); + if (!pattern.matcher(res).matches()) { + throw new AssertionError("Resource name \"" + res + "\" does not match pattern " + pattern); + } + checked.put("resourceName", true); + } + + public void resourceName(String name) { + String res = span.getResourceName().toString(); + if (!res.equals(name)) { + throw new AssertionError("Expected resourceName \"" + name + "\" but was \"" + res + "\""); + } + checked.put("resourceName", true); + } + + public void resourceNameMatches(java.util.function.Predicate eval) { + String res = span.getResourceName().toString(); + if (!eval.test(res)) { + throw new AssertionError("resourceName predicate did not match: " + res); + } + checked.put("resourceName", true); + } + + public void resourceNameContains(String... resourceNameParts) { + assertSpanNameContains(span.getResourceName().toString(), resourceNameParts); + checked.put("resourceName", true); + } + + public void duration(java.util.function.Predicate eval) { + long duration = span.getDurationNano(); + if (!eval.test(duration)) { + throw new AssertionError("duration predicate did not match: " + duration); + } + checked.put("duration", true); + } + + public void spanType(String type) { + if (span.getSpanType() == null) { + if (type != null) { + throw new AssertionError("Expected spanType \"" + type + "\" but was null"); + } + } else { + String actual = span.getSpanType().toString(); + if (type == null || !actual.equals(type)) { + throw new AssertionError("Expected spanType \"" + type + "\" but was \"" + actual + "\""); + } + } + if (span.getTags().get("span.type") != null) { + throw new AssertionError("span.type tag should be null"); + } + checked.put("spanType", true); + } + + public void parent() { + if (span.getParentId() != DDSpanId.ZERO) { + throw new AssertionError("Expected root span but parentId=" + span.getParentId()); + } + checked.put("parentId", true); + } + + public void parentSpanId(BigInteger parentId) { + long id = parentId == null ? 0 : DDSpanId.from(String.valueOf(parentId)); + if (span.getParentId() != id) { + throw new AssertionError("Expected parentId " + id + " but was " + span.getParentId()); + } + checked.put("parentId", true); + } + + public void traceId(BigInteger traceId) { + traceDDId(traceId != null ? DDTraceId.from(String.valueOf(traceId)) : null); + } + + public void traceDDId(DDTraceId traceId) { + if (!(traceId == null ? span.getTraceId() == null : traceId.equals(span.getTraceId()))) { + throw new AssertionError("Expected traceId " + traceId + " but was " + span.getTraceId()); + } + checked.put("traceId", true); + } + + public void childOf(DDSpan parent) { + if (span.getParentId() != parent.getSpanId()) { + throw new AssertionError( + "Expected parentId " + parent.getSpanId() + " but was " + span.getParentId()); + } + checked.put("parentId", true); + + if (!span.getTraceId().equals(parent.getTraceId())) { + throw new AssertionError( + "Expected traceId " + parent.getTraceId() + " but was " + span.getTraceId()); + } + checked.put("traceId", true); + } + + public void childOfPrevious() { + if (previous == null) { + throw new AssertionError("Previous span is null"); + } + childOf(previous); + } + + public void threadNameStartsWith(String threadName) { + Object tn = span.getTags().get("thread.name"); + if (!(tn instanceof String) || !((String) tn).startsWith(threadName)) { + throw new AssertionError( + "Thread name \"" + tn + "\" does not start with \"" + threadName + "\""); + } + } + + public void notChildOf(DDSpan parent) { + if (parent.getSpanId() == span.getParentId()) { + throw new AssertionError("Span is child of given parent"); + } + if (parent.getTraceId().equals(span.getTraceId())) { + throw new AssertionError("Span is in same trace as given parent"); + } + } + + public void errored(boolean errored) { + if (span.isError() != errored) { + throw new AssertionError("Expected errored=" + errored + " but was " + span.isError()); + } + checked.put("errored", true); + } + + public void topLevel(boolean topLevel) { + if (span.isTopLevel() != topLevel) { + throw new AssertionError("Expected topLevel=" + topLevel + " but was " + span.isTopLevel()); + } + checked.put("topLevel", true); + } + + public void measured(boolean measured) { + if (span.isMeasured() != measured) { + throw new AssertionError("Expected measured=" + measured + " but was " + span.isMeasured()); + } + checked.put("measured", true); + } + + private void assertDefaults() { + if (!Boolean.TRUE.equals(checked.get("spanType"))) { + spanType(null); + } + if (!Boolean.TRUE.equals(checked.get("errored"))) { + errored(false); + } + if (checkLinks) { + if (!Boolean.TRUE.equals(checked.get("links"))) { + if (span.getTags().get("_dd.span_links") != null) { + throw new AssertionError("_dd.span_links should be null"); + } + } + } + hasServiceName(); + } + + public void tags(Consumer spec) { + assertTags(span, spec); + } + + public void tags(boolean checkAllTags, Consumer spec) { + assertTags(span, spec, checkAllTags); + } + + public void ignoreSpanLinks() { + this.checkLinks = false; + } + + public void links(Consumer spec) { + checked.put("links", true); + assertLinks(span, spec); + } + + public void links(boolean checkAllLinks, Consumer spec) { + checked.put("links", true); + assertLinks(span, spec, checkAllLinks); + } + + public DDSpan getSpan() { + return span; + } +} diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java new file mode 100644 index 00000000000..49507e9877d --- /dev/null +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java @@ -0,0 +1,435 @@ +package datadog.trace.agent.test.asserts; + +import datadog.trace.api.Config; +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTags; +import datadog.trace.api.naming.SpanNaming; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.common.sampling.RateByServiceTraceSampler; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.common.writer.ddagent.TraceMapper; +import datadog.trace.core.DDSpan; +import java.io.Serializable; +import java.net.URI; +import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class TagsAssert { + + private final long spanParentId; + private final Map tags; + private final Set assertedTags = new TreeSet<>(); + + private TagsAssert(DDSpan span) { + this.spanParentId = span.getParentId(); + this.tags = span.getTags(); + } + + public static void assertTags( + DDSpan span, java.util.function.Consumer spec, boolean checkAllTags) { + + TagsAssert asserter = new TagsAssert(span); + spec.accept(asserter); + if (checkAllTags) { + asserter.assertTagsAllVerified(); + } + } + + public static void assertTags(DDSpan span, java.util.function.Consumer spec) { + + assertTags(span, spec, true); + } + + /** + * Check that, if the peer.service tag source has been set, it matches the provided one. + * + * @param sourceTag the source to match + */ + public void peerServiceFrom(String sourceTag) { + tag( + DDTags.PEER_SERVICE_SOURCE, + value -> + SpanNaming.instance().namingSchema().peerService().supports() + ? Objects.equals(value, sourceTag) + : value == null); + } + + public void withCustomIntegrationName(String integrationName) { + assertedTags.add(DDTags.DD_INTEGRATION); + Object value = tags.get(DDTags.DD_INTEGRATION); + if (value == null || !integrationName.equals(value.toString())) { + throw new AssertionError( + "Expected " + DDTags.DD_INTEGRATION + "=" + integrationName + " but was " + value); + } + } + + public void defaultTagsNoPeerService() { + defaultTags(false, false); + } + + public void defaultTagsNoPeerService(boolean distributedRootSpan) { + defaultTags(distributedRootSpan, false); + } + + public void isPresent(String name) { + tag(name, Objects::nonNull); + } + + public void arePresent(Collection tagNames) { + for (String name : tagNames) { + isPresent(name); + } + } + + public void isNotPresent(String name) { + tag(name, Objects::isNull); + } + + public void areNotPresent(Collection tagNames) { + for (String name : tagNames) { + isNotPresent(name); + } + } + + public void defaultTags() { + defaultTags(false, true); + } + + /** + * @param distributedRootSpan set to true if current span has a parent span but still considered + * 'root' for current service + */ + public void defaultTags(boolean distributedRootSpan, boolean checkPeerService) { + assertedTags.add("thread.name"); + assertedTags.add("thread.id"); + assertedTags.add(DDTags.RUNTIME_ID_TAG); + assertedTags.add(DDTags.LANGUAGE_TAG_KEY); + assertedTags.add(RateByServiceTraceSampler.SAMPLING_AGENT_RATE); + assertedTags.add(TraceMapper.SAMPLING_PRIORITY_KEY.toString()); + assertedTags.add("_sample_rate"); + assertedTags.add(DDTags.PID_TAG); + assertedTags.add(DDTags.SCHEMA_VERSION_TAG_KEY); + assertedTags.add(DDTags.PROFILING_ENABLED); + assertedTags.add(DDTags.PROFILING_CONTEXT_ENGINE); + assertedTags.add(DDTags.BASE_SERVICE); + assertedTags.add(DDTags.DSM_ENABLED); + assertedTags.add(DDTags.DJM_ENABLED); + assertedTags.add(DDTags.PARENT_ID); + assertedTags.add(DDTags.SPAN_LINKS); // this is checked by LinksAssert + for (String t : DDTags.REQUIRED_CODE_ORIGIN_TAGS) { + assertedTags.add(t); + } + + if (assertedTags.add(DDTags.DD_INTEGRATION) && tags.get(Tags.COMPONENT) != null) { + Object component = tags.get(Tags.COMPONENT); + Object integration = tags.get(DDTags.DD_INTEGRATION); + if (integration == null || !component.toString().equals(integration.toString())) { + throw new AssertionError( + "Component tag and dd.integration tag must match: " + component + " vs " + integration); + } + } + + if (tags.get("thread.name") == null) { + throw new AssertionError("thread.name tag is null"); + } + if (tags.get("thread.id") == null) { + throw new AssertionError("thread.id tag is null"); + } + + boolean isRoot = (DDSpanId.ZERO == spanParentId); + if (isRoot) { + Object actualSchema = tags.get(DDTags.SCHEMA_VERSION_TAG_KEY); + Object expectedSchema = SpanNaming.instance().version(); + if (!Objects.equals(actualSchema, expectedSchema)) { + throw new AssertionError( + "Schema version mismatch: expected " + expectedSchema + " but was " + actualSchema); + } + } + + if (isRoot || distributedRootSpan) { + if (!Objects.equals(tags.get(DDTags.RUNTIME_ID_TAG), Config.get().getRuntimeId())) { + throw new AssertionError( + "Runtime id mismatch: expected " + + Config.get().getRuntimeId() + + " but was " + + tags.get(DDTags.RUNTIME_ID_TAG)); + } + assertedTags.add(DDTags.TRACER_HOST); + if (!Objects.equals(tags.get(DDTags.TRACER_HOST), Config.get().getHostName())) { + throw new AssertionError( + "Tracer host mismatch: expected " + + Config.get().getHostName() + + " but was " + + tags.get(DDTags.TRACER_HOST)); + } + } else { + if (tags.get(DDTags.RUNTIME_ID_TAG) != null) { + throw new AssertionError("Non-root span should not have " + DDTags.RUNTIME_ID_TAG); + } + } + + String spanKind = (String) tags.get(Tags.SPAN_KIND); + boolean isServer = Tags.SPAN_KIND_SERVER.equals(spanKind); + + if (isRoot || distributedRootSpan || isServer) { + if (!Objects.equals(tags.get(DDTags.LANGUAGE_TAG_KEY), DDTags.LANGUAGE_TAG_VALUE)) { + throw new AssertionError( + "Language tag mismatch: expected " + + DDTags.LANGUAGE_TAG_VALUE + + " but was " + + tags.get(DDTags.LANGUAGE_TAG_KEY)); + } + } else { + if (tags.get(DDTags.LANGUAGE_TAG_KEY) != null) { + throw new AssertionError( + "Non-root, non-server span should not have " + DDTags.LANGUAGE_TAG_KEY); + } + } + + boolean shouldSetPeerService = + checkPeerService + && (Tags.SPAN_KIND_CLIENT.equals(spanKind) || Tags.SPAN_KIND_PRODUCER.equals(spanKind)); + + if (shouldSetPeerService && SpanNaming.instance().namingSchema().peerService().supports()) { + assertedTags.add(Tags.PEER_SERVICE); + assertedTags.add(DDTags.PEER_SERVICE_SOURCE); + Object peerService = tags.get(Tags.PEER_SERVICE); + Object source = tags.get(DDTags.PEER_SERVICE_SOURCE); + + if (peerService == null) { + throw new AssertionError("peer.service tag should not be null"); + } + Object sourceValue = tags.get(source); + if (!Objects.equals(peerService, sourceValue)) { + throw new AssertionError( + "peer.service mismatch: expected " + + sourceValue + + " (from " + + source + + ") but was " + + peerService); + } + } else { + if (tags.get(Tags.PEER_SERVICE) != null) { + throw new AssertionError("peer.service should be null"); + } + if (tags.get(DDTags.PEER_SERVICE_SOURCE) != null) { + throw new AssertionError("peer.service.source should be null"); + } + } + } + + public static void codeOriginTags(ListWriter writer) { + if (Config.get().isDebuggerCodeOriginEnabled()) { + List> traces = new ArrayList<>(writer); + + List spans = new ArrayList<>(); + for (List trace : traces) { + for (DDSpan span : trace) { + if (span.getTags().get(DDTags.DD_CODE_ORIGIN_TYPE) != null) { + spans.add(span); + } + } + } + + if (spans.isEmpty()) { + throw new AssertionError("Should have found at least one span with code origin"); + } + + for (DDSpan span : spans) { + assertTags( + span, + ta -> { + for (String tagName : DDTags.REQUIRED_CODE_ORIGIN_TAGS) { + Object value = ta.tags.get(tagName); + if (value == null) { + throw new AssertionError( + "Should have found " + tagName + " in span tags: " + ta.tags.keySet()); + } + } + }, + false); + } + } + } + + public void errorTags(Throwable error) { + errorTags(error.getClass(), error.getMessage()); + } + + public void errorTags(Class errorType) { + errorTags(errorType, null); + } + + public void errorTags(Class errorType, String message) { + tag( + "error.type", + value -> { + if (value == null) return false; + String typeName = value.toString(); + if (errorType.getName().equals(typeName)) { + return true; + } + try { + Class actual = Class.forName(typeName, false, getClass().getClassLoader()); + return errorType.isAssignableFrom(actual); + } catch (Throwable ignored) { + return false; + } + }); + + tag("error.stack", String.class); + + if (message != null) { + tag("error.message", message); + } + } + + public void urlTags(String url, List queryParams) { + tag( + "http.url", + value -> { + try { + String raw = value.toString().split("\\?", 2)[0]; + URI uri = new URI(raw); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + String path = uri.getPath(); + String baseURL = scheme + "://" + host + ":" + port + path; + return baseURL.equals(url); + } catch (Exception e) { + return false; + } + }); + + tag( + "http.query.string", + value -> { + String paramString = value == null ? null : value.toString(); + Set spanQueryParams = new HashSet<>(); + if (paramString != null && !paramString.isEmpty()) { + String[] pairs = paramString.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf('='); + if (idx > 0) { + spanQueryParams.add(pair.substring(0, idx)); + } else { + spanQueryParams.add(pair); + } + } + if (queryParams != null) { + for (String param : queryParams) { + if (!spanQueryParams.contains(param)) { + return false; + } + } + } + } else if (queryParams != null && !queryParams.isEmpty()) { + return false; + } + return true; + }); + } + + public void tag(String name, Pattern expected) { + if (expected == null) { + return; + } + assertedTags.add(name); + Object value = tag(name); + if (value == null || !expected.matcher(value.toString()).find()) { + throw new AssertionError( + "Tag \"" + name + "\": \"" + value + "\" does not match pattern \"" + expected + "\""); + } + } + + public void tag(String name, Class expected) { + if (expected == null) { + return; + } + assertedTags.add(name); + Object value = tag(name); + if (!expected.isInstance(value)) { + throw new AssertionError( + "Tag \"" + + name + + "\": instance check " + + expected.getName() + + " failed for \"" + + value + + "\" of class \"" + + (value == null ? "null" : value.getClass()) + + "\""); + } + } + + public void tag(String name, Predicate expected) { + if (expected == null) { + return; + } + assertedTags.add(name); + Object value = tag(name); + if (!expected.test(value)) { + throw new AssertionError( + "Tag \"" + name + "\": predicate " + expected + " failed with \"" + value + "\""); + } + } + + public void tag(String name, CharSequence expected) { + if (expected == null) { + return; + } + assertedTags.add(name); + Object value = tag(name); + String expectedStr = expected.toString(); + if (value == null || !expectedStr.equals(value.toString())) { + throw new AssertionError( + "Tag \"" + name + "\": \"" + value + "\" != \"" + expectedStr + "\""); + } + } + + public Object tag(String name) { + Object t = tags.get(name); + if (t instanceof CharSequence) { + return t.toString(); + } + return t; + } + + /* + public void methodMissing(String name, Object[] args) { + if (args == null || args.length == 0) { + throw new IllegalArgumentException( + "No value provided for dynamic tag assertion " + name); + } + tag(name, args[0]); + } + */ + + public boolean addTags(Map extraTags) { + for (Map.Entry e : extraTags.entrySet()) { + if (!(e.getValue() instanceof CharSequence)) { + throw new AssertionError( + "Unexpected value for tag \"" + e.getKey() + "\" Type: " + e.getValue().getClass()); + } + tag(e.getKey(), (CharSequence) e.getValue()); + } + return true; + } + + public void assertTagsAllVerified() { + Set remaining = new TreeSet<>(tags.keySet()); + remaining.removeAll(assertedTags); + if (!remaining.isEmpty()) { + throw new AssertionError( + "Unverified tags remain: " + + remaining + + " all=" + + tags.keySet() + + " asserted=" + + assertedTags); + } + } +} diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java new file mode 100644 index 00000000000..efe6c9028a8 --- /dev/null +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java @@ -0,0 +1,98 @@ +package datadog.trace.agent.test.asserts; + +import static datadog.trace.agent.test.asserts.SpanAssert.assertSpan; + +import datadog.trace.core.DDSpan; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public class TraceAssert { + + private List trace; + private final int size; + private final Set assertedIndexes = new HashSet<>(); + private final AtomicInteger spanAssertCount = new AtomicInteger(0); + + private TraceAssert(List trace) { + this.trace = Collections.unmodifiableList(trace); + this.size = trace.size(); + } + + public static final Comparator NAME_COMPARATOR = + new Comparator() { + @Override + public int compare(DDSpan o1, DDSpan o2) { + int compare = o1.getSpanName().toString().compareTo(o2.getSpanName().toString()); + return compare != 0 + ? compare + : String.valueOf(o1.getResourceName()) + .compareTo(String.valueOf(o2.getResourceName())); + } + }; + + public static void assertTrace(List trace, int expectedSize, Consumer spec) { + assertTrace(trace, expectedSize, null, spec); + } + + public static void assertTrace( + List trace, int expectedSize, Comparator sorter, Consumer spec) { + + // Copy to avoid concurrent modification with external code. + List copy = new ArrayList<>(trace); + if (copy.size() != expectedSize) { + throw new AssertionError("Expected " + expectedSize + " spans but got " + copy.size()); + } + if (sorter != null) { + copy.sort(sorter); + } + + TraceAssert asserter = new TraceAssert(copy); + spec.accept(asserter); + asserter.assertSpansAllVerified(); + } + + public DDSpan span(int index) { + return trace.get(index); + } + + public int nextSpanId() { + return spanAssertCount.getAndIncrement(); + } + + public void span(Consumer spec) { + span(nextSpanId(), spec); + } + + public void span(int index, Consumer spec) { + if (index >= size) { + throw new ArrayIndexOutOfBoundsException(index); + } + if (trace.size() != size) { + throw new ConcurrentModificationException("Trace modified during assertion"); + } + assertedIndexes.add(index); + + if (index > 0) { + assertSpan(trace.get(index), spec, trace.get(index - 1)); + } else { + assertSpan(trace.get(index), spec); + } + } + + public void assertSpansAllVerified() { + if (assertedIndexes.size() != size) { + throw new AssertionError( + "Not all spans were verified. Expected " + + size + + " but verified " + + assertedIndexes.size()); + } + } + + public void sortSpansByStart() { + List sorted = new ArrayList<>(trace); + sorted.sort(Comparator.comparingLong(DDSpan::getStartTimeNano)); + this.trace = Collections.unmodifiableList(sorted); + } +} From 2cd6a7b06aeb2d29d3c376a07c657714b0734ece Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 19 Dec 2025 13:51:44 +0100 Subject: [PATCH 2/2] LLM helps --- .../test/InstrumentationSpecification.groovy | 25 +- .../agent/test/asserts/ListWriterAssert.java | 95 ++++++ .../trace/agent/test/asserts/SpanAssert.java | 27 ++ .../trace/agent/test/asserts/TagsAssert.java | 271 +++++++++++++----- .../trace/agent/test/asserts/TraceAssert.java | 23 ++ 5 files changed, 368 insertions(+), 73 deletions(-) diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy index 47e67ffbafc..ac59e18c9e6 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy @@ -576,7 +576,17 @@ abstract class InstrumentationSpecification extends DDSpecification implements A options = "datadog.trace.agent.test.asserts.ListWriterAssert") @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST) final Closure spec) { - ListWriterAssert.assertTraces(TEST_WRITER, size, ignoreAdditionalTraces, spec) + // Ensure Groovy closure resolves methods (e.g., trace(), sortSpansByStart()) against ListWriterAssert + ListWriterAssert.assertTraces( + TEST_WRITER, + size, + ignoreAdditionalTraces, + { ListWriterAssert asserter -> + spec.delegate = asserter + spec.resolveStrategy = Closure.DELEGATE_FIRST + spec.call(asserter) + } as java.util.function.Consumer + ) } protected static final Comparator> SORT_TRACES_BY_ID = ListWriterAssert.SORT_TRACES_BY_ID @@ -591,7 +601,18 @@ abstract class InstrumentationSpecification extends DDSpecification implements A options = "datadog.trace.agent.test.asserts.ListWriterAssert") @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST) final Closure spec) { - ListWriterAssert.assertTraces(TEST_WRITER, size, false, traceSorter, spec) + // Ensure Groovy closure resolves methods against ListWriterAssert when a custom sorter is provided + ListWriterAssert.assertTraces( + TEST_WRITER, + size, + false, + traceSorter, + { ListWriterAssert asserter -> + spec.delegate = asserter + spec.resolveStrategy = Closure.DELEGATE_FIRST + spec.call(asserter) + } as java.util.function.Consumer + ) } void blockUntilChildSpansFinished(final int numberOfSpans) { diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java index a5fd32ede10..a74ade66d14 100644 --- a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/ListWriterAssert.java @@ -35,6 +35,22 @@ public static void assertTraces( assertTraces(writer, expectedSize, false, SORT_TRACES_BY_START, spec); } + // Groovy-friendly overload: allow passing a Closure and set delegate to ListWriterAssert + public static void assertTraces( + ListWriter writer, int expectedSize, groovy.lang.Closure spec) { + assertTraces( + writer, + expectedSize, + false, + SORT_TRACES_BY_START, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public static void assertTraces( ListWriter writer, int expectedSize, @@ -43,6 +59,25 @@ public static void assertTraces( assertTraces(writer, expectedSize, ignoreAdditionalTraces, SORT_TRACES_BY_START, spec); } + // Groovy-friendly overload with ignoreAdditionalTraces + public static void assertTraces( + ListWriter writer, + int expectedSize, + boolean ignoreAdditionalTraces, + groovy.lang.Closure spec) { + assertTraces( + writer, + expectedSize, + ignoreAdditionalTraces, + SORT_TRACES_BY_START, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public static void assertTraces( ListWriter writer, int expectedSize, @@ -113,6 +148,26 @@ public static void assertTraces( } } + // Groovy-friendly overload with explicit trace sorter + public static void assertTraces( + ListWriter writer, + int expectedSize, + boolean ignoreAdditionalTraces, + Comparator> traceSorter, + groovy.lang.Closure spec) { + assertTraces( + writer, + expectedSize, + ignoreAdditionalTraces, + traceSorter, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public void sortSpansByStart() { this.traces = traces.stream() @@ -133,10 +188,36 @@ public void trace(int expectedSize, Consumer spec) { trace(expectedSize, false, spec); } + // Groovy-friendly overload: allow passing a Closure and set delegate to TraceAssert + public void trace(int expectedSize, groovy.lang.Closure spec) { + trace( + expectedSize, + false, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public void trace(int expectedSize, boolean sortByName, Consumer spec) { trace(expectedSize, sortByName ? TraceAssert.NAME_COMPARATOR : null, spec); } + // Groovy-friendly overload with sortByName flag + public void trace(int expectedSize, boolean sortByName, groovy.lang.Closure spec) { + trace( + expectedSize, + sortByName ? TraceAssert.NAME_COMPARATOR : null, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public void trace(int expectedSize, Comparator sorter, Consumer spec) { int index = traceAssertCount.getAndIncrement(); if (index >= size) { @@ -149,6 +230,20 @@ public void trace(int expectedSize, Comparator sorter, Consumer sorter, groovy.lang.Closure spec) { + trace( + expectedSize, + sorter, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public void assertTracesAllVerified() { if (assertedIndexes.size() != size) { throw new AssertionError("Not all traces were verified."); diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java index d1a683b9892..c54c099cfc5 100644 --- a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/SpanAssert.java @@ -79,6 +79,15 @@ public void operationNameMatches(java.util.function.Predicate eval) { checked.put("operationName", true); } + // Groovy-friendly overload: allow calling operationName { it -> boolean } + public void operationName(groovy.lang.Closure eval) { + operationNameMatches( + (name) -> { + Object res = eval.call(name); + return res instanceof Boolean ? (Boolean) res : res != null; + }); + } + public void operationNameContains(String... operationNameParts) { assertSpanNameContains(span.getOperationName().toString(), operationNameParts); checked.put("operationName", true); @@ -108,6 +117,15 @@ public void resourceNameMatches(java.util.function.Predicate eval) { checked.put("resourceName", true); } + // Groovy-friendly overload: allow calling resourceName { it -> boolean } + public void resourceName(groovy.lang.Closure eval) { + resourceNameMatches( + (name) -> { + Object res = eval.call(name); + return res instanceof Boolean ? (Boolean) res : res != null; + }); + } + public void resourceNameContains(String... resourceNameParts) { assertSpanNameContains(span.getResourceName().toString(), resourceNameParts); checked.put("resourceName", true); @@ -248,6 +266,15 @@ public void tags(boolean checkAllTags, Consumer spec) { assertTags(span, spec, checkAllTags); } + // Groovy-friendly overloads: allow a Closure and set its delegate to TagsAssert + public void tags(groovy.lang.Closure spec) { + TagsAssert.assertTags(span, spec); + } + + public void tags(boolean checkAllTags, groovy.lang.Closure spec) { + TagsAssert.assertTags(span, spec, checkAllTags); + } + public void ignoreSpanLinks() { this.checkLinks = false; } diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java index 49507e9877d..c01763bb7c2 100644 --- a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TagsAssert.java @@ -14,18 +14,68 @@ import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; +import groovy.lang.Closure; +import groovy.lang.GroovyObject; +import groovy.lang.MetaClass; +import groovy.lang.GroovySystem; +import groovy.lang.MissingMethodException; + +public class TagsAssert implements GroovyObject { + private static final Pattern QUEST_MARK_PATTERN = Pattern.compile("\\?"); + private static final Pattern AMPERSAND_PATTERN = Pattern.compile("&"); -public class TagsAssert { private final long spanParentId; private final Map tags; private final Set assertedTags = new TreeSet<>(); + private transient MetaClass metaClass; private TagsAssert(DDSpan span) { this.spanParentId = span.getParentId(); this.tags = span.getTags(); } + // --- GroovyObject implementation to enable Groovy-style DSL delegation --- + @Override + public MetaClass getMetaClass() { + if (metaClass == null) { + metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass()); + } + return metaClass; + } + + @Override + public void setMetaClass(MetaClass metaClass) { + this.metaClass = metaClass; + } + + @Override + public Object invokeMethod(String name, Object args) { + try { + // Try normal dispatch first + return getMetaClass().invokeMethod(this, name, args); + } catch (MissingMethodException e) { + // Fallback to our dynamic tag assertion handler + if (args instanceof Object[]) { + methodMissing(name, (Object[]) args); + return null; + } else { + methodMissing(name, new Object[] {args}); + return null; + } + } + } + + @Override + public Object getProperty(String property) { + return getMetaClass().getProperty(this, property); + } + + @Override + public void setProperty(String property, Object newValue) { + getMetaClass().setProperty(this, property, newValue); + } + public static void assertTags( DDSpan span, java.util.function.Consumer spec, boolean checkAllTags) { @@ -41,6 +91,24 @@ public static void assertTags(DDSpan span, java.util.function.Consumer spec, boolean checkAllTags) { + + TagsAssert asserter = new TagsAssert(span); + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + if (checkAllTags) { + asserter.assertTagsAllVerified(); + } + } + + public static void assertTags(DDSpan span, groovy.lang.Closure spec) { + + assertTags(span, spec, true); + } + /** * Check that, if the peer.service tag source has been set, it matches the provided one. * @@ -49,7 +117,7 @@ public static void assertTags(DDSpan span, java.util.function.Consumer + (Predicate)value -> SpanNaming.instance().namingSchema().peerService().supports() ? Objects.equals(value, sourceTag) : value == null); @@ -73,7 +141,7 @@ public void defaultTagsNoPeerService(boolean distributedRootSpan) { } public void isPresent(String name) { - tag(name, Objects::nonNull); + tag(name, (Predicate)Objects::nonNull); } public void arePresent(Collection tagNames) { @@ -83,7 +151,7 @@ public void arePresent(Collection tagNames) { } public void isNotPresent(String name) { - tag(name, Objects::isNull); + tag(name, (Predicate)Objects::isNull); } public void areNotPresent(Collection tagNames) { @@ -96,6 +164,9 @@ public void defaultTags() { defaultTags(false, true); } + public void defaultTags(boolean distributedRootSpan) { + defaultTags(distributedRootSpan, true); + } /** * @param distributedRootSpan set to true if current span has a parent span but still considered * 'root' for current service @@ -262,10 +333,10 @@ public void errorTags(Class errorType) { errorTags(errorType, null); } - public void errorTags(Class errorType, String message) { + public void errorTags(Class errorType, Object message) { tag( "error.type", - value -> { + (Predicate) value -> { if (value == null) return false; String typeName = value.toString(); if (errorType.getName().equals(typeName)) { @@ -289,9 +360,9 @@ public void errorTags(Class errorType, String message) { public void urlTags(String url, List queryParams) { tag( "http.url", - value -> { + (Predicate)value -> { try { - String raw = value.toString().split("\\?", 2)[0]; + String raw = QUEST_MARK_PATTERN.split(value.toString(), 2)[0]; URI uri = new URI(raw); String scheme = uri.getScheme(); String host = uri.getHost(); @@ -306,11 +377,11 @@ public void urlTags(String url, List queryParams) { tag( "http.query.string", - value -> { + (Predicate)value -> { String paramString = value == null ? null : value.toString(); Set spanQueryParams = new HashSet<>(); if (paramString != null && !paramString.isEmpty()) { - String[] pairs = paramString.split("&"); + String[] pairs = AMPERSAND_PATTERN.split(paramString); for (String pair : pairs) { int idx = pair.indexOf('='); if (idx > 0) { @@ -333,60 +404,92 @@ public void urlTags(String url, List queryParams) { }); } - public void tag(String name, Pattern expected) { - if (expected == null) { - return; - } - assertedTags.add(name); - Object value = tag(name); - if (value == null || !expected.matcher(value.toString()).find()) { - throw new AssertionError( - "Tag \"" + name + "\": \"" + value + "\" does not match pattern \"" + expected + "\""); - } - } - - public void tag(String name, Class expected) { - if (expected == null) { - return; - } - assertedTags.add(name); - Object value = tag(name); - if (!expected.isInstance(value)) { - throw new AssertionError( - "Tag \"" - + name - + "\": instance check " - + expected.getName() - + " failed for \"" - + value - + "\" of class \"" - + (value == null ? "null" : value.getClass()) - + "\""); - } - } - - public void tag(String name, Predicate expected) { + public void tag(String name, Object expected) { if (expected == null) { return; } assertedTags.add(name); Object value = tag(name); - if (!expected.test(value)) { - throw new AssertionError( - "Tag \"" + name + "\": predicate " + expected + " failed with \"" + value + "\""); - } - } - public void tag(String name, CharSequence expected) { - if (expected == null) { - return; - } - assertedTags.add(name); - Object value = tag(name); - String expectedStr = expected.toString(); - if (value == null || !expectedStr.equals(value.toString())) { - throw new AssertionError( - "Tag \"" + name + "\": \"" + value + "\" != \"" + expectedStr + "\""); + if (expected instanceof Pattern) { + Pattern pattern = (Pattern) expected; + if (value == null || !pattern.matcher(value.toString()).find()) { + throw new AssertionError( + "Tag \"" + + name + + "\": \"" + + value + + "\" does not match pattern \"" + + pattern + + "\""); + } + } else if (expected instanceof Class) { + Class type = (Class) expected; + if (value == null || !type.isInstance(value)) { + throw new AssertionError( + "Tag \"" + + name + + "\": instance check " + + type + + " failed for \"" + + value + + "\" of class \"" + + (value == null ? "null" : value.getClass()) + + "\""); + } + } else if (expected instanceof Predicate) { + Predicate predicate = (Predicate) expected; + if (!predicate.test(value)) { + throw new AssertionError( + "Tag \"" + + name + + "\": predicate " + + expected + + " failed with \"" + + value + + "\""); + } + } else if (expected instanceof groovy.lang.Closure) { + // Support Groovy closures passed as expected values (e.g., in extraTags) + groovy.lang.Closure cl = (groovy.lang.Closure) expected; + java.util.function.Predicate predicate = + (v) -> { + Object res = cl.call(v); + return res instanceof Boolean ? (Boolean) res : res != null; + }; + if (!predicate.test(value)) { + throw new AssertionError( + "Tag \"" + + name + + "\": closure predicate " + + expected + + " failed with \"" + + value + + "\""); + } + } else if (expected instanceof CharSequence) { + String expectedStr = expected.toString(); + if (value == null || !expectedStr.equals(value.toString())) { + throw new AssertionError( + "Tag \"" + + name + + "\": \"" + + value + + "\" != \"" + + expectedStr + + "\""); + } + } else { + if (!Objects.equals(value, expected)) { + throw new AssertionError( + "Tag \"" + + name + + "\": \"" + + value + + "\" != \"" + + expected + + "\""); + } } } @@ -398,23 +501,49 @@ public Object tag(String name) { return t; } - /* - public void methodMissing(String name, Object[] args) { - if (args == null || args.length == 0) { - throw new IllegalArgumentException( - "No value provided for dynamic tag assertion " + name); - } - tag(name, args[0]); + // Support Groovy-style dynamic method calls inside the tags { } DSL, e.g. + // "$Tags.COMPONENT" "finatra" + // which translates to a call to methodMissing("component", ["finatra"]). + // Enabling this method ensures unqualified tag names are handled by the + // delegate (TagsAssert) instead of being resolved on the test class. + public void methodMissing(String name, Object[] args) { + if (args == null || args.length == 0) { + throw new IllegalArgumentException( + "No value provided for dynamic tag assertion " + name); + } + + Object arg = args[0]; + if (arg instanceof java.util.regex.Pattern) { + tag(name, (java.util.regex.Pattern) arg); + return; + } + if (arg instanceof Class) { + tag(name, (Class) arg); + return; + } + if (arg instanceof java.util.function.Predicate) { + // noinspection unchecked + tag(name, (java.util.function.Predicate) arg); + return; } - */ + if (arg instanceof groovy.lang.Closure) { + groovy.lang.Closure cl = (groovy.lang.Closure) arg; + tag( + name, + (java.util.function.Predicate) + (value) -> { + Object res = cl.call(value); + return res instanceof Boolean ? (Boolean) res : res != null; + }); + return; + } + // Fallback to string comparison + tag(name, arg == null ? null : arg.toString()); + } public boolean addTags(Map extraTags) { for (Map.Entry e : extraTags.entrySet()) { - if (!(e.getValue() instanceof CharSequence)) { - throw new AssertionError( - "Unexpected value for tag \"" + e.getKey() + "\" Type: " + e.getValue().getClass()); - } - tag(e.getKey(), (CharSequence) e.getValue()); + tag(e.getKey(), e.getValue()); } return true; } diff --git a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java index efe6c9028a8..60157ffd4ec 100644 --- a/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java +++ b/dd-java-agent/testing/src/main/java/datadog/trace/agent/test/asserts/TraceAssert.java @@ -64,6 +64,17 @@ public void span(Consumer spec) { span(nextSpanId(), spec); } + // Groovy-friendly overload: allow a Closure and set its delegate to SpanAssert + public void span(groovy.lang.Closure spec) { + span( + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public void span(int index, Consumer spec) { if (index >= size) { throw new ArrayIndexOutOfBoundsException(index); @@ -80,6 +91,18 @@ public void span(int index, Consumer spec) { } } + // Groovy-friendly overload with explicit index + public void span(int index, groovy.lang.Closure spec) { + span( + index, + (Consumer) + (asserter) -> { + spec.setDelegate(asserter); + spec.setResolveStrategy(groovy.lang.Closure.DELEGATE_FIRST); + spec.call(asserter); + }); + } + public void assertSpansAllVerified() { if (assertedIndexes.size() != size) { throw new AssertionError(