66import datadog .trace .test .agent .decoder .DecodedTrace ;
77import java .util .ArrayList ;
88import java .util .Comparator ;
9+ import java .util .Iterator ;
910import java .util .List ;
1011import java .util .function .UnaryOperator ;
1112
2122 * assertTraces(traces, trace(span().operationName("servlet.request").resourceName("GET /greeting")));
2223 * }</pre>
2324 *
24- * <p>Traces are matched positionally, in the order received by default. For order-independence pass
25- * {@link #SORT_BY_START_TIME} / {@link #SORT_BY_ROOT_SPAN_ID}; to assert only a subset of the
26- * received traces pass {@link #IGNORE_ADDITIONAL_TRACES}. The traces come from a {@code
27- * TraceBackend} (mock or test-agent), both decoded to {@link DecodedTrace}.
25+ * <p>By default traces are matched <em>positionally</em> (matcher <i>i</i> ↔ trace <i>i</i>), in
26+ * the order received — pass {@link #SORT_BY_START_TIME} / {@link #SORT_BY_ROOT_SPAN_ID} for a
27+ * stable order, and the count must match exactly. Pass {@link #IGNORE_ADDITIONAL_TRACES} to switch
28+ * to <em>order-independent subset</em> matching instead: each matcher must match a
29+ * <em>distinct</em> received trace, and any extra traces are ignored — the right mode when the
30+ * collection contains unrelated or non-deterministically-ordered traces (e.g. a distributed test
31+ * where connection-setup commands land as their own traces alongside the request traces you care
32+ * about). The traces come from a {@code TraceBackend} (mock or test-agent), both decoded to {@link
33+ * DecodedTrace}.
2834 */
2935public final class SmokeTraceAssertions {
3036 /** Sorts traces by the earliest span start time. */
@@ -35,7 +41,10 @@ public final class SmokeTraceAssertions {
3541 public static final Comparator <List <DecodedSpan >> TRACE_ROOT_SPAN_ID_COMPARATOR =
3642 Comparator .comparingLong (SmokeTraceAssertions ::rootSpanId );
3743
38- /** Do not fail when there are more traces than matchers (assert a subset). */
44+ /**
45+ * Switch to order-independent subset matching: each matcher must match a distinct received trace,
46+ * and extra traces are ignored (instead of the default count-exact positional matching).
47+ */
3948 public static final UnaryOperator <Options > IGNORE_ADDITIONAL_TRACES =
4049 Options ::ignoreAdditionalTraces ;
4150
@@ -58,94 +67,69 @@ public static void assertTraces(List<DecodedTrace> traces, TraceMatcher... match
5867 public static void assertTraces (
5968 List <DecodedTrace > traces , UnaryOperator <Options > options , TraceMatcher ... matchers ) {
6069 Options opts = options .apply (new Options ());
61- int expected = matchers .length ;
62- int actual = traces .size ();
63- if (opts .ignoreAdditionalTraces ) {
64- if (actual < expected ) {
65- throw new AssertionError ("Expected at least " + expected + " traces but got " + actual );
66- }
67- } else if (actual != expected ) {
68- throw new AssertionError ("Expected " + expected + " traces but got " + actual );
69- }
7070 // Copy each trace's spans to a mutable list so the matcher can sort/inspect them.
71- List <List <DecodedSpan >> spanLists = new ArrayList <>(actual );
71+ List <List <DecodedSpan >> spanLists = new ArrayList <>(traces . size () );
7272 for (DecodedTrace trace : traces ) {
7373 spanLists .add (new ArrayList <>(trace .getSpans ()));
7474 }
75- if (opts .sorter != null ) {
76- spanLists .sort (opts .sorter );
77- }
78- for (int i = 0 ; i < expected ; i ++) {
79- matchers [i ].assertTrace (spanLists .get (i ), i );
75+ if (opts .ignoreAdditionalTraces ) {
76+ assertContainsEach (spanLists , matchers );
77+ } else {
78+ assertPositionally (spanLists , opts , matchers );
8079 }
8180 }
8281
83- /**
84- * Asserts that some received trace contains a parent-child <em>chain</em> of spans matching
85- * {@code chain} in order: {@code chain[0]} matches a span (a trace root if it declares {@link
86- * SpanMatcher#root()}), and each {@code chain[i]} matches a direct child of {@code chain[i-1]}'s
87- * span. Unlike {@link #assertTraces}, this is a <em>subset</em> match — extra spans in the trace
88- * are ignored — which suits distributed traces whose full span set is large and timing-dependent
89- * (only span linkage, not exact shape, is asserted). Field constraints are declared per span; the
90- * chain declares the linkage, so the matchers should not also use {@code childOf*}.
91- */
92- public static void assertContainsChain (List <DecodedTrace > traces , SpanMatcher ... chain ) {
93- long found = countChainMatches (traces , chain );
94- if (found == 0 ) {
82+ // Order-independent subset: each matcher must match a distinct received trace; extras are
83+ // ignored.
84+ private static void assertContainsEach (
85+ List <List <DecodedSpan >> spanLists , TraceMatcher [] matchers ) {
86+ if (matchers .length > spanLists .size ()) {
9587 throw new AssertionError (
96- "No received trace contains a parent-child chain matching the "
97- + chain .length
98- + " given span matcher(s); traces: "
99- + traces );
100- }
101- }
102-
103- /**
104- * Counts how many received traces contain a parent-child chain matching {@code chain} (see {@link
105- * #assertContainsChain(List, SpanMatcher...)} for the chain semantics). Use to verify N
106- * independent operations each produced the expected trace — e.g. one distributed trace per
107- * request — rather than just that a single one did.
108- */
109- public static long countChainMatches (List <DecodedTrace > traces , SpanMatcher ... chain ) {
110- if (chain .length == 0 ) {
111- throw new IllegalArgumentException ("countChainMatches requires at least one span matcher" );
88+ "Expected at least "
89+ + matchers .length
90+ + " traces but got "
91+ + spanLists .size ()
92+ + ": "
93+ + spanLists );
11294 }
113- long count = 0 ;
114- for (DecodedTrace trace : traces ) {
115- if (containsChain (trace .getSpans (), chain )) {
116- count ++;
117- }
118- }
119- return count ;
120- }
121-
122- private static boolean containsChain (List <DecodedSpan > spans , SpanMatcher [] chain ) {
123- for (DecodedSpan first : spans ) {
124- if (chain [0 ].rootRequired () && first .getParentId () != 0L ) {
125- continue ;
95+ // Greedily assign each matcher to a distinct, not-yet-consumed trace. Adequate for smoke tests;
96+ // overlapping matchers (one strictly more general than another) could mis-assign — write them
97+ // specific enough to be unambiguous.
98+ List <List <DecodedSpan >> remaining = new ArrayList <>(spanLists );
99+ for (int i = 0 ; i < matchers .length ; i ++) {
100+ boolean matched = false ;
101+ for (Iterator <List <DecodedSpan >> it = remaining .iterator (); it .hasNext (); ) {
102+ if (matchers [i ].matches (it .next (), i )) {
103+ it .remove ();
104+ matched = true ;
105+ break ;
106+ }
126107 }
127- if (chain [0 ].matchesFields (first ) && matchesChainFrom (spans , chain , 1 , first )) {
128- return true ;
108+ if (!matched ) {
109+ throw new AssertionError (
110+ "No received trace matched trace matcher #"
111+ + i
112+ + " among "
113+ + spanLists .size ()
114+ + " received trace(s): "
115+ + spanLists );
129116 }
130117 }
131- return false ;
132118 }
133119
134- // Depth-first, backtracking: extend the chain by a direct child of `parent` matching
135- // chain[index].
136- private static boolean matchesChainFrom (
137- List < DecodedSpan > spans , SpanMatcher [] chain , int index , DecodedSpan parent ) {
138- if ( index == chain . length ) {
139- return true ;
120+ // Count-exact positional: matcher i ↔ trace i, after the optional trace sort.
121+ private static void assertPositionally (
122+ List < List < DecodedSpan >> spanLists , Options opts , TraceMatcher [] matchers ) {
123+ if ( spanLists . size () != matchers . length ) {
124+ throw new AssertionError (
125+ "Expected " + matchers . length + " traces but got " + spanLists . size ()) ;
140126 }
141- for (DecodedSpan child : spans ) {
142- if (child .getParentId () == parent .getSpanId ()
143- && chain [index ].matchesFields (child )
144- && matchesChainFrom (spans , chain , index + 1 , child )) {
145- return true ;
146- }
127+ if (opts .sorter != null ) {
128+ spanLists .sort (opts .sorter );
129+ }
130+ for (int i = 0 ; i < matchers .length ; i ++) {
131+ matchers [i ].assertTrace (spanLists .get (i ), i );
147132 }
148- return false ;
149133 }
150134
151135 private static long earliestStart (List <DecodedSpan > spans ) {
0 commit comments