66import datadog .trace .test .agent .decoder .DecodedTrace ;
77import java .util .ArrayList ;
88import java .util .Comparator ;
9- import java .util .Iterator ;
109import java .util .List ;
1110import java .util .function .UnaryOperator ;
1211
2221 * assertTraces(traces, trace(span().operationName("servlet.request").resourceName("GET /greeting")));
2322 * }</pre>
2423 *
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}.
24+ * <p>A single matching algorithm is tuned by three orthogonal, combinable {@link Options}:
25+ *
26+ * <ul>
27+ * <li>{@link #SORT_BY_START_TIME} / {@link #SORT_BY_ROOT_SPAN_ID} (the {@code sorter}) — sort the
28+ * received traces before matching.
29+ * <li>{@link #UNORDERED} — a matcher may match <em>any</em> distinct (unconsumed) trace rather
30+ * than the one at its position. Without a sorter this is fully order-independent;
31+ * <em>with</em> a sorter it is forward-only (once a matcher takes the trace at sorted
32+ * position <i>p</i>, the next matcher can only look at positions after <i>p</i>) — i.e. match
33+ * as a subsequence in sorted order.
34+ * <li>{@link #IGNORE_ADDITIONAL_TRACES} — don't require every received trace to be matched; extra
35+ * traces are ignored (a subset assertion). Without it, the counts must be equal.
36+ * </ul>
37+ *
38+ * <p>The default (no options) is thus count-exact positional matching (matcher <i>i</i> ↔ trace
39+ * <i>i</i>, received order). Combine flags with the fluent {@link Options} — e.g. {@code o ->
40+ * o.unordered().ignoreAdditionalTraces()} — to assert only the traces you care about in a
41+ * collection that also holds unrelated or non-deterministically-ordered ones (e.g. a distributed
42+ * test where connection-setup commands land as their own traces). The traces come from a {@code
43+ * TraceBackend} (mock or test-agent), both decoded to {@link DecodedTrace}.
3444 */
3545public final class SmokeTraceAssertions {
3646 /** Sorts traces by the earliest span start time. */
@@ -41,13 +51,13 @@ public final class SmokeTraceAssertions {
4151 public static final Comparator <List <DecodedSpan >> TRACE_ROOT_SPAN_ID_COMPARATOR =
4252 Comparator .comparingLong (SmokeTraceAssertions ::rootSpanId );
4353
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- */
54+ /** Don't require every received trace to be matched — ignore the extras (a subset assertion). */
4855 public static final UnaryOperator <Options > IGNORE_ADDITIONAL_TRACES =
4956 Options ::ignoreAdditionalTraces ;
5057
58+ /** Let each matcher match any distinct trace rather than the one at its position. */
59+ public static final UnaryOperator <Options > UNORDERED = Options ::unordered ;
60+
5161 /** Sorts traces by earliest span start time before matching. */
5262 public static final UnaryOperator <Options > SORT_BY_START_TIME =
5363 options -> options .sorter (TRACE_START_TIME_COMPARATOR );
@@ -63,73 +73,76 @@ public static void assertTraces(List<DecodedTrace> traces, TraceMatcher... match
6373 assertTraces (traces , identity (), matchers );
6474 }
6575
66- /** As {@link #assertTraces(List, TraceMatcher...)} with the given options. */
76+ /**
77+ * As {@link #assertTraces(List, TraceMatcher...)} with the given options. One matching pass,
78+ * tuned by {@code sorter} / {@link Options#unordered() unordered} / {@link
79+ * Options#ignoreAdditionalTraces() ignoreAdditionalTraces} — see the class doc for the flag
80+ * semantics.
81+ */
6782 public static void assertTraces (
6883 List <DecodedTrace > traces , UnaryOperator <Options > options , TraceMatcher ... matchers ) {
6984 Options opts = options .apply (new Options ());
7085 // Copy each trace's spans to a mutable list so the matcher can sort/inspect them.
71- List <List <DecodedSpan >> spanLists = new ArrayList <>(traces .size ());
86+ List <List <DecodedSpan >> pool = new ArrayList <>(traces .size ());
7287 for (DecodedTrace trace : traces ) {
73- spanLists .add (new ArrayList <>(trace .getSpans ()));
88+ pool .add (new ArrayList <>(trace .getSpans ()));
7489 }
75- if (opts .ignoreAdditionalTraces ) {
76- assertContainsEach (spanLists , matchers );
77- } else {
78- assertPositionally (spanLists , opts , matchers );
79- }
80- }
81-
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 ()) {
87- throw new AssertionError (
88- "Expected at least "
89- + matchers .length
90- + " traces but got "
91- + spanLists .size ()
92- + ": "
93- + spanLists );
90+ if (opts .sorter != null ) {
91+ pool .sort (opts .sorter );
9492 }
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 );
93+ // Walk the matchers, each consuming a distinct trace. `floor` is the lowest pool index a match
94+ // may use: it advances past each match to enforce order, except in fully-unordered mode (no
95+ // sorter) where it stays 0 so any remaining trace is a candidate. `consumed` keeps matches
96+ // distinct when the floor doesn't advance.
97+ boolean [] consumed = new boolean [pool .size ()];
98+ int matched = 0 ;
99+ int floor = 0 ;
99100 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- }
107- }
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 );
101+ int idx = findMatch (pool , consumed , floor , opts , matchers [i ], i );
102+ consumed [idx ] = true ;
103+ matched ++;
104+ if (!opts .unordered || opts .sorter != null ) {
105+ floor = idx + 1 ;
116106 }
117107 }
118- }
119-
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 ) {
108+ if (!opts .ignoreAdditionalTraces && matched != pool .size ()) {
124109 throw new AssertionError (
125- "Expected " + matchers .length + " traces but got " + spanLists .size ());
110+ "Expected " + matchers .length + " traces but got " + pool .size () + ": " + pool );
126111 }
127- if (opts .sorter != null ) {
128- spanLists .sort (opts .sorter );
112+ }
113+
114+ // Finds the trace this matcher consumes, or throws. Scans from `floor`, skipping consumed traces
115+ // and (unless matching strictly positionally) non-matching ones. A strictly-positional matcher
116+ // that fails is re-run with the throwing assertion so the failure names the offending span.
117+ private static int findMatch (
118+ List <List <DecodedSpan >> pool ,
119+ boolean [] consumed ,
120+ int floor ,
121+ Options opts ,
122+ TraceMatcher matcher ,
123+ int i ) {
124+ boolean strictPositional = !opts .unordered && !opts .ignoreAdditionalTraces ;
125+ for (int j = floor ; j < pool .size (); j ++) {
126+ if (consumed [j ]) {
127+ continue ;
128+ }
129+ if (matcher .matches (pool .get (j ), i )) {
130+ return j ;
131+ }
132+ if (strictPositional ) {
133+ break ; // the trace at this position must match; no skipping ahead
134+ }
129135 }
130- for ( int i = 0 ; i < matchers . length ; i ++ ) {
131- matchers [ i ] .assertTrace (spanLists .get (i ), i );
136+ if ( strictPositional && floor < pool . size () && ! consumed [ floor ] ) {
137+ matcher .assertTrace (pool .get (floor ), i ); // throws with the per-span reason
132138 }
139+ throw new AssertionError (
140+ "No received trace matched trace matcher #"
141+ + i
142+ + " among "
143+ + pool .size ()
144+ + " received trace(s): "
145+ + pool );
133146 }
134147
135148 private static long earliestStart (List <DecodedSpan > spans ) {
@@ -149,16 +162,24 @@ private static long rootSpanId(List<DecodedSpan> spans) {
149162 return spans .isEmpty () ? 0L : spans .get (0 ).getSpanId ();
150163 }
151164
152- /** Trace-collection matching options. */
165+ /** Trace-collection matching options; see the class doc for how they combine . */
153166 public static final class Options {
154167 boolean ignoreAdditionalTraces = false ;
168+ boolean unordered = false ;
155169 Comparator <List <DecodedSpan >> sorter = null ; // null => keep received order
156170
171+ /** Ignore received traces beyond those the matchers consume (a subset assertion). */
157172 public Options ignoreAdditionalTraces () {
158173 this .ignoreAdditionalTraces = true ;
159174 return this ;
160175 }
161176
177+ /** Let each matcher match any distinct trace (forward-only when a {@link #sorter} is set). */
178+ public Options unordered () {
179+ this .unordered = true ;
180+ return this ;
181+ }
182+
162183 public Options sorter (Comparator <List <DecodedSpan >> sorter ) {
163184 this .sorter = sorter ;
164185 return this ;
0 commit comments