Skip to content

Commit d6d9076

Browse files
committed
WIP
1 parent d42ded1 commit d6d9076

2 files changed

Lines changed: 90 additions & 113 deletions

File tree

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

Lines changed: 69 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package datadog.smoketest.trace;
22

3+
import static java.lang.Long.MAX_VALUE;
4+
import static java.lang.Math.min;
35
import static java.util.function.UnaryOperator.identity;
6+
import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure;
47

58
import datadog.trace.test.agent.decoder.DecodedSpan;
69
import datadog.trace.test.agent.decoder.DecodedTrace;
@@ -44,13 +47,9 @@
4447
*/
4548
public 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
}

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

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,6 @@ void ignoresAdditionalTraces() {
9191
trace(span().operationName("root-a").root()));
9292
}
9393

94-
@Test
95-
void sortsTracesByRootSpanId() {
96-
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(TWO_TRACES);
97-
// Root-id order is [root-b (400), root-a (500)], the reverse of the received order.
98-
assertTraces(
99-
traces,
100-
SmokeTraceAssertions.SORT_BY_ROOT_SPAN_ID,
101-
trace(span().operationName("root-b").root()),
102-
trace(span().operationName("root-a").root()));
103-
}
104-
10594
@Test
10695
void sortsByParentChainRegardlessOfStartTime() {
10796
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(CHAIN_TRACE);
@@ -157,27 +146,27 @@ void unorderedRequiresDistinctTraces() {
157146
trace(span().operationName("root-a").root())));
158147
}
159148

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-
}
149+
// @Test
150+
// void unorderedWithSorterIsForwardOnly() {
151+
// List<DecodedTrace> traces = TestAgentTraceDecoder.decode(TWO_TRACES);
152+
// // Sorted by root span id => [root-b (400), root-a (500)]. With a sorter, an unordered match is
153+
// // forward-only: matchers in sorted order match...
154+
// assertTraces(
155+
// traces,
156+
// options -> options.unordered().sorter(SmokeTraceAssertions.TRACE_ROOT_SPAN_ID_COMPARATOR),
157+
// trace(span().operationName("root-b").root()),
158+
// trace(span().operationName("root-a").root()));
159+
// // ...but reversed they don't (root-a is matched at position 1, leaving nothing after it).
160+
// assertThrows(
161+
// AssertionError.class,
162+
// () ->
163+
// assertTraces(
164+
// traces,
165+
// options ->
166+
// options.unordered().sorter(SmokeTraceAssertions.TRACE_ROOT_SPAN_ID_COMPARATOR),
167+
// trace(span().operationName("root-a").root()),
168+
// trace(span().operationName("root-b").root())));
169+
// }
181170

182171
private static String spanJson(String name, long id, long parent, long start) {
183172
return "{\"service\":\"s\",\"name\":\""

0 commit comments

Comments
 (0)