Skip to content

Commit d42ded1

Browse files
committed
WIP
1 parent 783fae3 commit d42ded1

3 files changed

Lines changed: 133 additions & 87 deletions

File tree

dd-smoke-tests/spring-boot-rabbit/src/test/java/datadog/smoketest/SpringBootRabbitSmokeTest.java

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package datadog.smoketest;
22

3-
import static datadog.smoketest.trace.SmokeTraceAssertions.IGNORE_ADDITIONAL_TRACES;
43
import static datadog.smoketest.trace.SpanMatcher.span;
54
import static datadog.smoketest.trace.TraceMatcher.SORT_BY_PARENT_CHAIN;
65
import static datadog.smoketest.trace.TraceMatcher.trace;
@@ -55,14 +54,15 @@
5554
* spring-rabbit-0 spring.consume Receiver.receiveMessage (sender consumes reply)
5655
* </pre>
5756
*
58-
* <p>The whole collection is asserted with the standard {@link Traces#assertTraces} DSL in {@link
59-
* SmokeTraceAssertions#IGNORE_ADDITIONAL_TRACES order-independent subset} mode: each matcher
60-
* matches a distinct received trace and extras are ignored. This is stronger than the Groovy
61-
* set-membership check — each round-trip trace is matched count-exact (all 12 spans, in {@link
62-
* TraceMatcher#SORT_BY_PARENT_CHAIN parent-chain order}), verifying every AMQP operation
63-
* (publish/deliver/consume, both directions) <em>and</em> its cross-service linkage — while staying
64-
* robust to the timing-dependent extras (the broker emits its connection-setup commands and per-ack
65-
* traces as their own single-span traces, in non-deterministic count and order).
57+
* <p>The whole collection is asserted with the standard {@link Traces#assertTraces} DSL in
58+
* {@linkplain SmokeTraceAssertions order-independent subset} mode ({@code
59+
* unordered().ignoreAdditionalTraces()}): each matcher matches a distinct received trace and extras
60+
* are ignored. This is stronger than the Groovy set-membership check — each round-trip trace is
61+
* matched count-exact (all 12 spans, in {@link TraceMatcher#SORT_BY_PARENT_CHAIN parent-chain
62+
* order}), verifying every AMQP operation (publish/deliver/consume, both directions) <em>and</em>
63+
* its cross-service linkage — while staying robust to the timing-dependent extras (the broker emits
64+
* its connection-setup commands and per-ack traces as their own single-span traces, in
65+
* non-deterministic count and order).
6666
*
6767
* <p>Two design points, informed by inspecting the live trace collection:
6868
*
@@ -123,10 +123,10 @@ void roundtripsProduceFullAmqpTraceStructure() throws IOException {
123123
}
124124
}
125125

126-
// One order-independent assertion over the whole collection (IGNORE_ADDITIONAL_TRACES): each
127-
// matcher must match a distinct received trace; unrelated/duplicate traces (extra acks, etc.)
128-
// are ignored. Assert one full round-trip trace per message plus each service's
129-
// connection-setup/ack commands.
126+
// One order-independent subset assertion over the whole collection (unordered +
127+
// ignoreAdditionalTraces): each matcher must match a distinct received trace, and
128+
// unrelated/duplicate traces (extra acks, etc.) are ignored. Assert one full round-trip trace
129+
// per message plus each service's connection-setup/ack commands.
130130
List<TraceMatcher> expected = new ArrayList<>();
131131
for (int i = 0; i < MESSAGES.length; i++) {
132132
expected.add(roundTrip()); // one full round-trip trace per message => all are verified
@@ -139,7 +139,9 @@ void roundtripsProduceFullAmqpTraceStructure() throws IOException {
139139
agent
140140
.traces()
141141
.assertTraces(
142-
TIMEOUT_SECONDS, IGNORE_ADDITIONAL_TRACES, expected.toArray(new TraceMatcher[0]));
142+
TIMEOUT_SECONDS,
143+
o -> o.unordered().ignoreAdditionalTraces(),
144+
expected.toArray(new TraceMatcher[0]));
143145
}
144146

145147
// The full distributed round-trip as one strict parent->child chain across both services and the

dd-smoke-tests/src/main/java/datadog/smoketest/trace/SmokeTraceAssertions.java

Lines changed: 89 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import datadog.trace.test.agent.decoder.DecodedTrace;
77
import java.util.ArrayList;
88
import java.util.Comparator;
9-
import java.util.Iterator;
109
import java.util.List;
1110
import java.util.function.UnaryOperator;
1211

@@ -22,15 +21,26 @@
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
*/
3545
public 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;

dd-smoke-tests/src/test/java/datadog/smoketest/trace/SmokeMatcherTest.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,30 +132,53 @@ void parentChainRejectsNonLinearTrace() {
132132
}
133133

134134
@Test
135-
void ignoreAdditionalTracesMatchesAnyOrder() {
135+
void unorderedMatchesAnyOrder() {
136136
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(TWO_TRACES);
137-
// Matchers in the opposite order of receipt still each find their trace (order-independent).
137+
// Matchers in the opposite order of receipt still each find their trace (no sorter => any
138+
// order).
138139
assertTraces(
139140
traces,
140-
SmokeTraceAssertions.IGNORE_ADDITIONAL_TRACES,
141+
options -> options.unordered().ignoreAdditionalTraces(),
141142
trace(span().operationName("root-b").root()),
142143
trace(span().operationName("root-a").root()));
143144
}
144145

145146
@Test
146-
void ignoreAdditionalTracesRequiresDistinctTraces() {
147+
void unorderedRequiresDistinctTraces() {
147148
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(TWO_TRACES);
148149
// Two matchers for the same trace can't both match: only one root-a trace exists.
149150
assertThrows(
150151
AssertionError.class,
151152
() ->
152153
assertTraces(
153154
traces,
154-
SmokeTraceAssertions.IGNORE_ADDITIONAL_TRACES,
155+
options -> options.unordered().ignoreAdditionalTraces(),
155156
trace(span().operationName("root-a").root()),
156157
trace(span().operationName("root-a").root())));
157158
}
158159

160+
@Test
161+
void unorderedWithSorterIsForwardOnly() {
162+
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(TWO_TRACES);
163+
// Sorted by root span id => [root-b (400), root-a (500)]. With a sorter, an unordered match is
164+
// forward-only: matchers in sorted order match...
165+
assertTraces(
166+
traces,
167+
options -> options.unordered().sorter(SmokeTraceAssertions.TRACE_ROOT_SPAN_ID_COMPARATOR),
168+
trace(span().operationName("root-b").root()),
169+
trace(span().operationName("root-a").root()));
170+
// ...but reversed they don't (root-a is matched at position 1, leaving nothing after it).
171+
assertThrows(
172+
AssertionError.class,
173+
() ->
174+
assertTraces(
175+
traces,
176+
options ->
177+
options.unordered().sorter(SmokeTraceAssertions.TRACE_ROOT_SPAN_ID_COMPARATOR),
178+
trace(span().operationName("root-a").root()),
179+
trace(span().operationName("root-b").root())));
180+
}
181+
159182
private static String spanJson(String name, long id, long parent, long start) {
160183
return "{\"service\":\"s\",\"name\":\""
161184
+ name

0 commit comments

Comments
 (0)