Skip to content

Commit 4654d52

Browse files
committed
feat(test): Add instrumentation test for JUnit
1 parent 09f5899 commit 4654d52

File tree

11 files changed

+491
-34
lines changed

11 files changed

+491
-34
lines changed

dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package datadog.trace.agent.test;
22

3+
import static java.util.function.Function.identity;
34
import static org.junit.jupiter.api.Assertions.assertNull;
45
import static org.junit.jupiter.api.Assertions.assertTrue;
56

67
import datadog.instrument.classinject.ClassInjector;
8+
import datadog.trace.agent.test.assertions.TraceAssertions;
9+
import datadog.trace.agent.test.assertions.TraceMatcher;
710
import datadog.trace.agent.tooling.AgentInstaller;
811
import datadog.trace.agent.tooling.InstrumenterModule;
912
import datadog.trace.agent.tooling.TracerInstaller;
@@ -22,11 +25,19 @@
2225
import java.util.ServiceLoader;
2326
import java.util.concurrent.TimeUnit;
2427
import java.util.concurrent.TimeoutException;
28+
import java.util.function.Function;
2529
import net.bytebuddy.agent.ByteBuddyAgent;
2630
import org.junit.jupiter.api.AfterEach;
2731
import org.junit.jupiter.api.BeforeEach;
2832
import org.junit.jupiter.api.extension.ExtendWith;
29-
33+
import org.opentest4j.AssertionFailedError;
34+
35+
/**
36+
* This class is an experimental base to run instrumentation tests using JUnit Jupiter. It is still
37+
* early development, and the overall API is expected to change to leverage its extension model. The
38+
* current implementation is inspired and kept close to it Groovy / Spock counterpart, the {@code
39+
* InstrumentationSpecification}.
40+
*/
3041
@ExtendWith(TestClassShadowingExtension.class)
3142
public abstract class AbstractInstrumentationTest {
3243
static final Instrumentation INSTRUMENTATION = ByteBuddyAgent.getInstrumentation();
@@ -100,6 +111,33 @@ public void tearDown() {
100111
this.transformerLister = null;
101112
}
102113

114+
/**
115+
* Checks the structure of the traces captured from the test agent.
116+
*
117+
* @param matchers The matchers to verify the trace collection, one matcher by expected trace.
118+
*/
119+
protected void assertTraces(TraceMatcher... matchers) {
120+
assertTraces(identity(), matchers);
121+
}
122+
123+
/**
124+
* Checks the structure of the traces captured from the test agent.
125+
*
126+
* @param options The {@link TraceAssertions.Options} to configure the checks.
127+
* @param matchers The matchers to verify the trace collection, one matcher by expected trace.
128+
*/
129+
protected void assertTraces(
130+
Function<TraceAssertions.Options, TraceAssertions.Options> options,
131+
TraceMatcher... matchers) {
132+
int expectedTraceCount = matchers.length;
133+
try {
134+
this.writer.waitForTraces(expectedTraceCount);
135+
} catch (InterruptedException | TimeoutException e) {
136+
throw new AssertionFailedError("Timeout while waiting for traces", e);
137+
}
138+
TraceAssertions.assertTraces(this.writer, options, matchers);
139+
}
140+
103141
protected void blockUntilChildSpansFinished(final int numberOfSpans) {
104142
blockUntilChildSpansFinished(this.tracer.activeSpan(), numberOfSpans);
105143
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package datadog.trace.agent.test.assertions;
2+
3+
import java.util.Optional;
4+
5+
public class Is<T> implements Matcher<T> {
6+
private final T expected;
7+
8+
Is(T expected) {
9+
this.expected = expected;
10+
}
11+
12+
@Override
13+
public Optional<T> expected() {
14+
return Optional.of(this.expected);
15+
}
16+
17+
@Override
18+
public String message() {
19+
return "Unexpected value";
20+
}
21+
22+
@Override
23+
public boolean test(T t) {
24+
return this.expected.equals(t);
25+
}
26+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package datadog.trace.agent.test.assertions;
2+
3+
import java.util.Optional;
4+
import java.util.function.Predicate;
5+
6+
public interface Matcher<T> extends Predicate<T> {
7+
Optional<T> expected();
8+
9+
String message();
10+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package datadog.trace.agent.test.assertions;
2+
3+
import java.util.function.Predicate;
4+
import java.util.regex.Pattern;
5+
6+
public class Matchers {
7+
public static <T> Matcher<T> is(T expected) {
8+
return new Is<>(expected);
9+
}
10+
11+
public static <T> Matcher<T> nonNull() {
12+
return new NonNull<>();
13+
}
14+
15+
public static Matcher<CharSequence> matches(String regex) {
16+
return new Matches(Pattern.compile(regex));
17+
}
18+
19+
public static Matcher<CharSequence> matches(Pattern pattern) {
20+
return new Matches(pattern);
21+
}
22+
23+
public static <T> Matcher<T> validates(Predicate<T> validator) {
24+
return new Validates<>(validator);
25+
}
26+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package datadog.trace.agent.test.assertions;
2+
3+
import java.util.Optional;
4+
import java.util.regex.Pattern;
5+
6+
public class Matches implements Matcher<CharSequence> {
7+
private final Pattern pattern;
8+
9+
Matches(Pattern pattern) {
10+
this.pattern = pattern;
11+
}
12+
13+
@Override
14+
public Optional<CharSequence> expected() {
15+
return Optional.empty();
16+
}
17+
18+
@Override
19+
public String message() {
20+
return "Non matching value";
21+
}
22+
23+
@Override
24+
public boolean test(CharSequence s) {
25+
return this.pattern.matcher(s).matches();
26+
}
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package datadog.trace.agent.test.assertions;
2+
3+
import java.util.Optional;
4+
5+
public class NonNull<T> implements Matcher<T> {
6+
@Override
7+
public Optional<T> expected() {
8+
return Optional.empty();
9+
}
10+
11+
@Override
12+
public String message() {
13+
return "Non-null value expected";
14+
}
15+
16+
@Override
17+
public boolean test(T t) {
18+
return t != null;
19+
}
20+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package datadog.trace.agent.test.assertions;
2+
3+
import static datadog.trace.agent.test.assertions.Matchers.is;
4+
import static datadog.trace.agent.test.assertions.Matchers.matches;
5+
import static datadog.trace.agent.test.assertions.Matchers.nonNull;
6+
import static datadog.trace.agent.test.assertions.Matchers.validates;
7+
import static java.time.temporal.ChronoUnit.NANOS;
8+
9+
import datadog.trace.core.DDSpan;
10+
import java.time.Duration;
11+
import java.util.Optional;
12+
import java.util.function.Predicate;
13+
import java.util.regex.Pattern;
14+
import org.opentest4j.AssertionFailedError;
15+
16+
public final class SpanMatcher {
17+
private Matcher<Long> idMatcher;
18+
private Matcher<Long> parentIdMatcher;
19+
private Matcher<String> serviceNameMatcher;
20+
private Matcher<CharSequence> operationNameMatcher;
21+
private Matcher<CharSequence> resourceNameMatcher;
22+
private Matcher<Duration> durationMatcher;
23+
24+
private static final Matcher<Long> CHILD_OF_PREVIOUS_MATCHER = is(0L);
25+
26+
private SpanMatcher() {}
27+
28+
public static SpanMatcher span() {
29+
return new SpanMatcher();
30+
}
31+
32+
public SpanMatcher withId(long id) {
33+
this.idMatcher = is(id);
34+
return this;
35+
}
36+
37+
public SpanMatcher isRoot() {
38+
return childOf(0L);
39+
}
40+
41+
public SpanMatcher childOf(long parentId) {
42+
this.parentIdMatcher = is(parentId);
43+
return this;
44+
}
45+
46+
public SpanMatcher childOfPrevious() {
47+
this.parentIdMatcher = CHILD_OF_PREVIOUS_MATCHER;
48+
return this;
49+
}
50+
51+
public SpanMatcher hasServiceName() {
52+
this.serviceNameMatcher = nonNull();
53+
return this;
54+
}
55+
56+
public SpanMatcher withServiceName(String serviceName) {
57+
this.serviceNameMatcher = is(serviceName);
58+
return this;
59+
}
60+
61+
public SpanMatcher withOperationName(String operationName) {
62+
this.operationNameMatcher = is(operationName);
63+
return this;
64+
}
65+
66+
public SpanMatcher operationNameMatching(Pattern pattern) {
67+
this.operationNameMatcher = matches(pattern);
68+
return this;
69+
}
70+
71+
public SpanMatcher withResourceName(String resourceName) {
72+
this.resourceNameMatcher = is(resourceName);
73+
return this;
74+
}
75+
76+
public SpanMatcher resourceNameMatching(Pattern pattern) {
77+
this.resourceNameMatcher = matches(pattern);
78+
return this;
79+
}
80+
81+
public SpanMatcher resourceNameMatching(Predicate<CharSequence> validator) {
82+
this.resourceNameMatcher = validates(validator);
83+
return this;
84+
}
85+
86+
public SpanMatcher durationShorterThan(Duration duration) {
87+
this.durationMatcher = validates(d -> d.compareTo(duration) < 0);
88+
return this;
89+
}
90+
91+
public SpanMatcher durationLongerThan(Duration duration) {
92+
this.durationMatcher = validates(d -> d.compareTo(duration) > 0);
93+
return this;
94+
}
95+
96+
public SpanMatcher durationMatching(Predicate<Duration> validator) {
97+
this.durationMatcher = validates(validator);
98+
return this;
99+
}
100+
101+
public void assertSpan(DDSpan span, DDSpan previousSpan) {
102+
// Apply parent id matcher from the previous span
103+
if (this.parentIdMatcher == CHILD_OF_PREVIOUS_MATCHER) {
104+
this.parentIdMatcher = is(previousSpan.getSpanId());
105+
}
106+
// Assert span values
107+
assertValue(this.idMatcher, span.getSpanId(), "Expected identifier");
108+
assertValue(this.parentIdMatcher, span.getParentId(), "Expected parent identifier");
109+
assertValue(this.serviceNameMatcher, span.getServiceName(), "Expected service name");
110+
assertValue(this.operationNameMatcher, span.getOperationName(), "Expected operation name");
111+
assertValue(this.resourceNameMatcher, span.getResourceName(), "Expected resource name");
112+
assertValue(
113+
this.durationMatcher, Duration.of(span.getDurationNano(), NANOS), "Expected duration");
114+
// TODO Add more values to test (tags, links, ...)
115+
}
116+
117+
private <T> void assertValue(Matcher<T> matcher, T value, String message) {
118+
if (matcher != null && !matcher.test(value)) {
119+
Optional<T> expected = matcher.expected();
120+
if (expected.isPresent()) {
121+
throw new AssertionFailedError(message + ". " + matcher.message(), expected.get(), value);
122+
} else {
123+
throw new AssertionFailedError(message + ": " + value + ". " + matcher.message());
124+
}
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)