Skip to content

Commit 798798a

Browse files
committed
feat(testing): Add decoded span assert rules
1 parent d14912e commit 798798a

4 files changed

Lines changed: 885 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package datadog.smoketest.trace;
2+
3+
import static java.lang.Long.MAX_VALUE;
4+
import static java.lang.Math.min;
5+
import static java.util.function.UnaryOperator.identity;
6+
import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure;
7+
8+
import datadog.trace.test.agent.decoder.DecodedSpan;
9+
import datadog.trace.test.agent.decoder.DecodedTrace;
10+
import java.util.ArrayList;
11+
import java.util.BitSet;
12+
import java.util.Comparator;
13+
import java.util.List;
14+
import java.util.function.UnaryOperator;
15+
16+
/**
17+
* This class is a helper class to verify trace structure.
18+
*
19+
* <p>To check for trace structure, use the static factory methods: {@link #assertTraces(List,
20+
* TraceMatcher...)} with the expected {@link TraceMatcher}s (one per trace), or {@link
21+
* #assertTraces(List, UnaryOperator, TraceMatcher...)} to configure the checks with a {@link
22+
* Options} object.
23+
*
24+
* <p>The following predefined configurations:
25+
*
26+
* <ul>
27+
* <li>{@link #IGNORE_ADDITIONAL_TRACES} ignores additional traces if there are more than
28+
* expected,
29+
* <li>{@link #SORT_BY_START_TIME} sorts traces by their start time,
30+
* <li>{@link #UNORDERED} allows matchers to match any distinct trace rather than the one at its
31+
* position.
32+
* </ul>
33+
*/
34+
public final class SmokeTraceAssertions {
35+
/*
36+
* Trace comparators.
37+
*/
38+
/** Trace comparator to sort by start time. */
39+
public static final Comparator<DecodedTrace> TRACE_START_TIME_COMPARATOR =
40+
Comparator.comparingLong(SmokeTraceAssertions::earliestStart);
41+
42+
/*
43+
* Trace assertion options.
44+
*/
45+
/** Ignores additional traces. If there are more traces than expected, do not fail. */
46+
public static final UnaryOperator<Options> IGNORE_ADDITIONAL_TRACES =
47+
Options::ignoreAdditionalTraces;
48+
49+
/** Allows matchers to match any distinct trace rather than the one at its position. */
50+
public static final UnaryOperator<Options> UNORDERED = Options::unorder;
51+
52+
/** Sorts traces by start time. */
53+
public static final UnaryOperator<Options> SORT_BY_START_TIME =
54+
options -> options.sort(TRACE_START_TIME_COMPARATOR);
55+
56+
private SmokeTraceAssertions() {}
57+
58+
/**
59+
* Checks the structure of a trace collection.
60+
*
61+
* @param traces The trace collection to check.
62+
* @param matchers The matchers to verify the trace collection, one matcher by expected trace.
63+
*/
64+
public static void assertTraces(List<DecodedTrace> traces, TraceMatcher... matchers) {
65+
assertTraces(traces, identity(), matchers);
66+
}
67+
68+
/**
69+
* Checks the structure of a trace collection.
70+
*
71+
* @param traces The trace collection to check.
72+
* @param options The {@link Options} to configure the checks.
73+
* @param matchers The matchers to verify the trace collection, one matcher by expected trace.
74+
*/
75+
public static void assertTraces(
76+
List<DecodedTrace> traces, UnaryOperator<Options> options, TraceMatcher... matchers) {
77+
Options opts = options.apply(new Options());
78+
// Check trace count first
79+
int traceCount = traces.size();
80+
if (opts.ignoreAdditionalTraces) {
81+
if (traceCount < matchers.length) {
82+
assertionFailure()
83+
.message("Not enough of traces")
84+
.expected(matchers.length)
85+
.actual(traceCount)
86+
.buildAndThrow();
87+
}
88+
} else {
89+
if (traceCount != matchers.length) {
90+
assertionFailure()
91+
.message("Invalid number of traces")
92+
.expected(matchers.length)
93+
.actual(traceCount)
94+
.buildAndThrow();
95+
}
96+
}
97+
// Apply sorter
98+
if (opts.sorter != null) {
99+
traces = new ArrayList<>(traces);
100+
traces.sort(opts.sorter);
101+
}
102+
// Assert traces
103+
boolean strictPositional = !opts.unordered && !opts.ignoreAdditionalTraces;
104+
BitSet skippedTraces = new BitSet(traceCount);
105+
for (int matcherIndex = 0; matcherIndex < matchers.length; matcherIndex++) {
106+
TraceMatcher matcher = matchers[matcherIndex];
107+
if (strictPositional) {
108+
matcher.assertTrace(traces.get(matcherIndex).getSpans(), matcherIndex);
109+
continue;
110+
}
111+
boolean matched = false;
112+
for (int traceIndex = skippedTraces.nextClearBit(0); traceIndex < traceCount; traceIndex++) {
113+
if (skippedTraces.get(traceIndex)) {
114+
continue;
115+
}
116+
try {
117+
matcher.assertTrace(traces.get(traceIndex).getSpans(), matcherIndex);
118+
matched = true;
119+
if (opts.unordered) {
120+
skippedTraces.set(traceIndex);
121+
} else {
122+
skippedTraces.set(0, traceIndex + 1);
123+
}
124+
break;
125+
} catch (AssertionError ignored) {
126+
// Swallow assertion errors, keep looking for a match
127+
}
128+
}
129+
if (!matched) {
130+
assertionFailure().message("No trace matches matcher # " + matcherIndex).buildAndThrow();
131+
}
132+
}
133+
}
134+
135+
private static long earliestStart(DecodedTrace trace) {
136+
long start = MAX_VALUE;
137+
for (DecodedSpan span : trace.getSpans()) {
138+
start = min(start, span.getStart());
139+
}
140+
return start == MAX_VALUE ? 0L : start;
141+
}
142+
143+
public static final class Options {
144+
boolean ignoreAdditionalTraces = false;
145+
boolean unordered = false;
146+
Comparator<DecodedTrace> sorter = null;
147+
148+
public Options ignoreAdditionalTraces() {
149+
this.ignoreAdditionalTraces = true;
150+
return this;
151+
}
152+
153+
/**
154+
* Matches each matcher against any distinct trace rather than the one at its position.
155+
*
156+
* <p>Assignment is greedy: each matcher takes the first still-unmatched trace it accepts. This
157+
* can spuriously fail when matchers accept overlapping sets of traces and a valid distinct
158+
* assignment exists only under a different pairing. Keep matchers specific enough to be
159+
* unambiguous.
160+
*/
161+
public Options unorder() {
162+
this.unordered = true;
163+
this.sorter = null;
164+
return this;
165+
}
166+
167+
public Options sort(Comparator<DecodedTrace> sorter) {
168+
this.unordered = false;
169+
this.sorter = sorter;
170+
return this;
171+
}
172+
}
173+
}

0 commit comments

Comments
 (0)