11package datadog .smoketest .trace ;
22
3+ import static java .lang .Long .MAX_VALUE ;
4+ import static java .lang .Math .min ;
35import static java .util .function .UnaryOperator .identity ;
6+ import static org .junit .jupiter .api .AssertionFailureBuilder .assertionFailure ;
47
58import datadog .trace .test .agent .decoder .DecodedSpan ;
69import datadog .trace .test .agent .decoder .DecodedTrace ;
4447 */
4548public final class SmokeTraceAssertions {
4649 /** Sorts traces by the earliest span start time. */
47- public static final Comparator <List < DecodedSpan > > TRACE_START_TIME_COMPARATOR =
50+ public static final Comparator <DecodedTrace > TRACE_START_TIME_COMPARATOR =
4851 Comparator .comparingLong (SmokeTraceAssertions ::earliestStart );
4952
50- /** Sorts traces by their root span id (the span with no parent). */
51- public static final Comparator <List <DecodedSpan >> TRACE_ROOT_SPAN_ID_COMPARATOR =
52- Comparator .comparingLong (SmokeTraceAssertions ::rootSpanId );
53-
5453 /** Don't require every received trace to be matched — ignore the extras (a subset assertion). */
5554 public static final UnaryOperator <Options > IGNORE_ADDITIONAL_TRACES =
5655 Options ::ignoreAdditionalTraces ;
@@ -62,111 +61,100 @@ public final class SmokeTraceAssertions {
6261 public static final UnaryOperator <Options > SORT_BY_START_TIME =
6362 options -> options .sorter (TRACE_START_TIME_COMPARATOR );
6463
65- /** Sorts traces by root span id before matching. */
66- public static final UnaryOperator <Options > SORT_BY_ROOT_SPAN_ID =
67- options -> options .sorter (TRACE_ROOT_SPAN_ID_COMPARATOR );
68-
6964 private SmokeTraceAssertions () {}
7065
71- /** Asserts the traces against the matchers, one matcher per expected trace, in received order. */
66+ /**
67+ * Checks the structure of a trace collection.
68+ *
69+ * @param traces The trace collection to check.
70+ * @param matchers The matchers to verify the trace collection, one matcher by expected trace.
71+ */
7272 public static void assertTraces (List <DecodedTrace > traces , TraceMatcher ... matchers ) {
7373 assertTraces (traces , identity (), matchers );
7474 }
7575
7676 /**
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.
77+ * Checks the structure of a trace collection.
78+ *
79+ * @param traces The trace collection to check.
80+ * @param options The {@link Options} to configure the checks.
81+ * @param matchers The matchers to verify the trace collection, one matcher by expected trace.
8182 */
8283 public static void assertTraces (
8384 List <DecodedTrace > traces , UnaryOperator <Options > options , TraceMatcher ... matchers ) {
8485 Options opts = options .apply (new Options ());
85- // Copy each trace's spans to a mutable list so the matcher can sort/inspect them.
86- List <List <DecodedSpan >> pool = new ArrayList <>(traces .size ());
87- for (DecodedTrace trace : traces ) {
88- pool .add (new ArrayList <>(trace .getSpans ()));
86+ // Check trace count first
87+ int traceCount = traces .size ();
88+ if (opts .ignoreAdditionalTraces ) {
89+ if (traceCount < matchers .length ) {
90+ assertionFailure ()
91+ .message ("Not enough of traces" )
92+ .expected (matchers .length )
93+ .actual (traceCount )
94+ .buildAndThrow ();
95+ }
96+ } else {
97+ if (traceCount != matchers .length ) {
98+ assertionFailure ()
99+ .message ("Invalid number of traces" )
100+ .expected (matchers .length )
101+ .actual (traceCount )
102+ .buildAndThrow ();
103+ }
89104 }
105+ // Apply sorter
90106 if (opts .sorter != null ) {
91- pool .sort (opts .sorter );
107+ traces = new ArrayList <>(traces );
108+ traces .sort (opts .sorter );
92109 }
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 ;
110+ boolean strictPositional = !opts .unordered && !opts .ignoreAdditionalTraces ;
111+ // Record matched traces for unordered mode
112+ boolean [] traceMatched = new boolean [traceCount ];
113+ // Record last matching trace index for ignoreAdditionalTraces mode
99114 int floor = 0 ;
100- for (int i = 0 ; i < matchers .length ; i ++) {
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 ;
106- }
107- }
108- if (!opts .ignoreAdditionalTraces && matched != pool .size ()) {
109- throw new AssertionError (
110- "Expected " + matchers .length + " traces but got " + pool .size () + ": " + pool );
111- }
112- }
113115
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 ]) {
116+ for (int matcherIndex = 0 ; matcherIndex < matchers .length ; matcherIndex ++) {
117+ TraceMatcher matcher = matchers [matcherIndex ];
118+ if (strictPositional ) {
119+ matcher .assertTrace (traces .get (matcherIndex ).getSpans (), matcherIndex );
127120 continue ;
128121 }
129- if (matcher .matches (pool .get (j ), i )) {
130- return j ;
122+
123+ boolean matched = false ;
124+ for (int traceIndex = floor ; traceIndex < traceCount ; traceIndex ++) {
125+ // Skipped already matched traces - unordered mode only
126+ if (traceMatched [traceIndex ]) {
127+ continue ;
128+ }
129+ if (matcher .matches (traces .get (traceIndex ).getSpans (), matcherIndex )) {
130+ matched = true ;
131+ if (opts .unordered ) {
132+ traceMatched [traceIndex ] = true ;
133+ } else {
134+ floor = traceIndex + 1 ;
135+ }
136+ break ;
137+ }
131138 }
132- if (strictPositional ) {
133- break ; // the trace at this position must match; no skipping ahead
139+ if (! matched ) {
140+ assertionFailure (). message ( "No trace matches matcher # " + matcherIndex ). buildAndThrow ();
134141 }
135142 }
136- if (strictPositional && floor < pool .size () && !consumed [floor ]) {
137- matcher .assertTrace (pool .get (floor ), i ); // throws with the per-span reason
138- }
139- throw new AssertionError (
140- "No received trace matched trace matcher #"
141- + i
142- + " among "
143- + pool .size ()
144- + " received trace(s): "
145- + pool );
146143 }
147144
148- private static long earliestStart (List <DecodedSpan > spans ) {
149- long earliest = Long .MAX_VALUE ;
150- for (DecodedSpan span : spans ) {
151- earliest = Math .min (earliest , span .getStart ());
152- }
153- return spans .isEmpty () ? 0L : earliest ;
154- }
155-
156- private static long rootSpanId (List <DecodedSpan > spans ) {
157- for (DecodedSpan span : spans ) {
158- if (span .getParentId () == 0L ) {
159- return span .getSpanId ();
160- }
145+ private static long earliestStart (DecodedTrace trace ) {
146+ long start = MAX_VALUE ;
147+ for (DecodedSpan span : trace .getSpans ()) {
148+ start = min (start , span .getStart ());
161149 }
162- return spans . isEmpty () ? 0L : spans . get ( 0 ). getSpanId () ;
150+ return start == MAX_VALUE ? 0L : start ;
163151 }
164152
165153 /** Trace-collection matching options; see the class doc for how they combine. */
166154 public static final class Options {
167155 boolean ignoreAdditionalTraces = false ;
168156 boolean unordered = false ;
169- Comparator <List < DecodedSpan > > sorter = null ; // null => keep received order
157+ Comparator <DecodedTrace > sorter = null ; // null => keep received order
170158
171159 /** Ignore received traces beyond those the matchers consume (a subset assertion). */
172160 public Options ignoreAdditionalTraces () {
@@ -180,7 +168,7 @@ public Options unordered() {
180168 return this ;
181169 }
182170
183- public Options sorter (Comparator <List < DecodedSpan > > sorter ) {
171+ public Options sorter (Comparator <DecodedTrace > sorter ) {
184172 this .sorter = sorter ;
185173 return this ;
186174 }
0 commit comments