Skip to content

Commit 30c99de

Browse files
Prevent double reporting of Scalatest events when using SBT with test forking (#8682)
1 parent c9b9007 commit 30c99de

File tree

6 files changed

+146
-22
lines changed

6 files changed

+146
-22
lines changed

.circleci/config.continue.yml.j2

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ instrumentation_modules: &instrumentation_modules "dd-java-agent/instrumentation
3636
debugger_modules: &debugger_modules "dd-java-agent/agent-debugger|dd-java-agent/agent-bootstrap|dd-java-agent/agent-builder|internal-api|communication|dd-trace-core"
3737
profiling_modules: &profiling_modules "dd-java-agent/agent-profiling"
3838

39-
default_system_tests_commit: &default_system_tests_commit 2799fa982318da14c9d3e5f722abdc670d2802c3
39+
default_system_tests_commit: &default_system_tests_commit 761b9e7a82ffb136c4653a4d1623d120d67b005b
4040

4141
parameters:
4242
nightly:
@@ -932,7 +932,7 @@ jobs:
932932
command: |
933933
cd system-tests
934934
DD_SITE=datadoghq.com DD_API_KEY=$SYSTEM_TESTS_E2E_DD_API_KEY DD_APPLICATION_KEY=$SYSTEM_TESTS_E2E_DD_APP_KEY ./run.sh INTEGRATIONS
935-
935+
936936
- run:
937937
name: Run IDM Crossed Tracing Libraries propagation tests for messaging
938938
environment:
@@ -975,7 +975,7 @@ jobs:
975975
no_output_timeout: 5m
976976
command: |
977977
cd system-tests
978-
export DD_API_KEY=$SYSTEM_TESTS_E2E_DD_API_KEY
978+
export DD_API_KEY=$SYSTEM_TESTS_E2E_DD_API_KEY
979979
./run.sh DEBUGGER_SCENARIOS
980980
981981
- run:

dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/DatadogReporter.java

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,17 @@ private static void stop(Event event) {
8686

8787
private static void onSuiteStart(SuiteStarting event) {
8888
int runStamp = event.ordinal().runStamp();
89-
RunContext context = RunContext.getOrCreate(runStamp);
90-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
89+
RunContext context = RunContext.get(runStamp);
90+
if (context == null) {
91+
return;
92+
}
9193

9294
String testSuiteName = event.suiteId();
9395
Class<?> testClass = ScalatestUtils.getClass(event.suiteClassName());
9496
Collection<String> categories = Collections.emptyList();
9597
boolean parallelized = true;
9698

99+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
97100
eventHandler.onTestSuiteStart(
98101
new TestSuiteDescriptor(testSuiteName, testClass),
99102
testSuiteName,
@@ -108,30 +111,40 @@ private static void onSuiteStart(SuiteStarting event) {
108111

109112
private static void onSuiteFinish(SuiteCompleted event) {
110113
int runStamp = event.ordinal().runStamp();
111-
RunContext context = RunContext.getOrCreate(runStamp);
112-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
114+
RunContext context = RunContext.get(runStamp);
115+
if (context == null) {
116+
return;
117+
}
113118

114119
String testSuiteName = event.suiteId();
115120
Class<?> testClass = ScalatestUtils.getClass(event.suiteClassName());
121+
122+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
116123
eventHandler.onTestSuiteFinish(new TestSuiteDescriptor(testSuiteName, testClass), null);
117124
}
118125

119126
private static void onSuiteAbort(SuiteAborted event) {
120127
int runStamp = event.ordinal().runStamp();
121-
RunContext context = RunContext.getOrCreate(runStamp);
122-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
128+
RunContext context = RunContext.get(runStamp);
129+
if (context == null) {
130+
return;
131+
}
123132

124133
String testSuiteName = event.suiteId();
125134
Class<?> testClass = ScalatestUtils.getClass(event.suiteClassName());
126135
Throwable throwable = event.throwable().getOrElse(null);
136+
137+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
127138
eventHandler.onTestSuiteFailure(new TestSuiteDescriptor(testSuiteName, testClass), throwable);
128139
eventHandler.onTestSuiteFinish(new TestSuiteDescriptor(testSuiteName, testClass), null);
129140
}
130141

131142
private static void onTestStart(TestStarting event) {
132143
int runStamp = event.ordinal().runStamp();
133-
RunContext context = RunContext.getOrCreate(runStamp);
134-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
144+
RunContext context = RunContext.get(runStamp);
145+
if (context == null) {
146+
return;
147+
}
135148

136149
String testSuiteName = event.suiteId();
137150
String testName = event.testName();
@@ -146,6 +159,7 @@ private static void onTestStart(TestStarting event) {
146159
}
147160
Class<?> testClass = ScalatestUtils.getClass(event.suiteClassName());
148161

162+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
149163
eventHandler.onTestStart(
150164
new TestSuiteDescriptor(testSuiteName, testClass),
151165
new TestDescriptor(testSuiteName, testClass, testName, testParameters, testQualifier),
@@ -161,8 +175,10 @@ private static void onTestStart(TestStarting event) {
161175

162176
private static void onTestSuccess(TestSucceeded event) {
163177
int runStamp = event.ordinal().runStamp();
164-
RunContext context = RunContext.getOrCreate(runStamp);
165-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
178+
RunContext context = RunContext.get(runStamp);
179+
if (context == null) {
180+
return;
181+
}
166182

167183
String testSuiteName = event.suiteId();
168184
Class<?> testClass = ScalatestUtils.getClass(event.suiteClassName());
@@ -175,13 +191,16 @@ private static void onTestSuccess(TestSucceeded event) {
175191
TestIdentifier testIdentifier = new TestIdentifier(testSuiteName, testName, null);
176192
TestExecutionHistory executionHistory = context.popExecutionHistory(testIdentifier);
177193

194+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
178195
eventHandler.onTestFinish(testDescriptor, null, executionHistory);
179196
}
180197

181198
private static void onTestFailure(TestFailed event) {
182199
int runStamp = event.ordinal().runStamp();
183-
RunContext context = RunContext.getOrCreate(runStamp);
184-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
200+
RunContext context = RunContext.get(runStamp);
201+
if (context == null) {
202+
return;
203+
}
185204

186205
String testSuiteName = event.suiteId();
187206
Class<?> testClass = ScalatestUtils.getClass(event.suiteClassName());
@@ -195,14 +214,17 @@ private static void onTestFailure(TestFailed event) {
195214
TestIdentifier testIdentifier = new TestIdentifier(testSuiteName, testName, null);
196215
TestExecutionHistory executionHistory = context.popExecutionHistory(testIdentifier);
197216

217+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
198218
eventHandler.onTestFailure(testDescriptor, throwable);
199219
eventHandler.onTestFinish(testDescriptor, null, executionHistory);
200220
}
201221

202222
private static void onTestIgnore(TestIgnored event) {
203223
int runStamp = event.ordinal().runStamp();
204-
RunContext context = RunContext.getOrCreate(runStamp);
205-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
224+
RunContext context = RunContext.get(runStamp);
225+
if (context == null) {
226+
return;
227+
}
206228

207229
String testSuiteName = event.suiteId();
208230
String testName = event.testName();
@@ -214,6 +236,7 @@ private static void onTestIgnore(TestIgnored event) {
214236
TestIdentifier skippableTest = new TestIdentifier(testSuiteName, testName, null);
215237
SkipReason reason = context.getSkipReason(skippableTest);
216238

239+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
217240
eventHandler.onTestIgnore(
218241
new TestSuiteDescriptor(testSuiteName, testClass),
219242
new TestDescriptor(testSuiteName, testClass, testName, testParameters, testQualifier),
@@ -228,8 +251,10 @@ private static void onTestIgnore(TestIgnored event) {
228251

229252
private static void onTestCancel(TestCanceled event) {
230253
int runStamp = event.ordinal().runStamp();
231-
RunContext context = RunContext.getOrCreate(runStamp);
232-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
254+
RunContext context = RunContext.get(runStamp);
255+
if (context == null) {
256+
return;
257+
}
233258

234259
String testSuiteName = event.suiteId();
235260
String testName = event.testName();
@@ -241,6 +266,7 @@ private static void onTestCancel(TestCanceled event) {
241266

242267
TestDescriptor testDescriptor =
243268
new TestDescriptor(testSuiteName, testClass, testName, testParameters, testQualifier);
269+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
244270
if (throwable instanceof SuppressedTestFailedException) {
245271
eventHandler.onTestFailure(testDescriptor, throwable.getCause());
246272
} else {
@@ -255,8 +281,10 @@ private static void onTestCancel(TestCanceled event) {
255281

256282
private static void onTestPending(TestPending event) {
257283
int runStamp = event.ordinal().runStamp();
258-
RunContext context = RunContext.getOrCreate(runStamp);
259-
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
284+
RunContext context = RunContext.get(runStamp);
285+
if (context == null) {
286+
return;
287+
}
260288

261289
String testSuiteName = event.suiteId();
262290
String testName = event.testName();
@@ -267,6 +295,7 @@ private static void onTestPending(TestPending event) {
267295

268296
TestDescriptor testDescriptor =
269297
new TestDescriptor(testSuiteName, testClass, testName, testParameters, testQualifier);
298+
TestEventsHandler<TestSuiteDescriptor, TestDescriptor> eventHandler = context.getEventHandler();
270299
eventHandler.onTestSkip(testDescriptor, reason);
271300

272301
TestIdentifier testIdentifier = new TestIdentifier(testSuiteName, testName, null);

dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/RunContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ public static RunContext getOrCreate(int runStamp) {
3131
return CONTEXTS.computeIfAbsent(runStamp, RunContext::new);
3232
}
3333

34+
public static RunContext get(int runStamp) {
35+
return CONTEXTS.get(runStamp);
36+
}
37+
3438
public static void destroy(int runStamp) {
3539
RunContext context = CONTEXTS.remove(runStamp);
36-
context.destroy();
40+
if (context != null) {
41+
context.destroy();
42+
}
3743
}
3844

3945
private final int runStamp;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package datadog.trace.instrumentation.scalatest;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
5+
6+
import com.google.auto.service.AutoService;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import datadog.trace.agent.tooling.InstrumenterModule;
9+
import datadog.trace.api.Config;
10+
import datadog.trace.api.config.CiVisibilityConfig;
11+
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
12+
import java.util.Set;
13+
import net.bytebuddy.asm.Advice;
14+
import org.scalatest.Reporter;
15+
16+
@AutoService(InstrumenterModule.class)
17+
public class ScalatestForkInstrumentation extends InstrumenterModule.CiVisibility
18+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
19+
20+
public ScalatestForkInstrumentation() {
21+
super("ci-visibility", "scalatest");
22+
}
23+
24+
@Override
25+
public boolean isApplicable(Set<TargetSystem> enabledSystems) {
26+
return super.isApplicable(enabledSystems)
27+
&& Config.get().isCiVisibilityScalatestForkMonitorEnabled();
28+
}
29+
30+
@Override
31+
public String instrumentedType() {
32+
return "org.scalatest.tools.Framework";
33+
}
34+
35+
@Override
36+
public String[] helperClassNames() {
37+
return new String[] {};
38+
}
39+
40+
@Override
41+
public void methodAdvice(MethodTransformer transformer) {
42+
transformer.applyAdvice(
43+
named("runner").and(takesArgument(1, String[].class)),
44+
ScalatestForkInstrumentation.class.getName() + "$RunnerAdvice");
45+
}
46+
47+
/**
48+
* This instrumentation is needed to prevent double-reporting of Scalatest events when using SBT
49+
* and Test/fork. Current version of the code cannot detect such cases automatically, so it has to
50+
* be activated explicitly by setting {@link
51+
* CiVisibilityConfig#CIVISIBILITY_SCALATEST_FORK_MONITOR_ENABLED}.
52+
*
53+
* <p>When using SBT with test forking, Scalatest reporter is activated both in the parent and in
54+
* the child process. We only want to instrument the child process (since it is actually executing
55+
* the tests) and not the parent one. The way to distinguish between the two is by examining the
56+
* "remoteArgs" passed to the runner method: the args are non-empty in the child process, and
57+
* empty in the parent one.
58+
*
59+
* <p>The instrumentation works by increasing the Reporter.class counter in the call depth thread
60+
* local map. The counter is decreased when the runner method exits.
61+
*
62+
* <p>Since the tracing instrumentation is checking the counter value to avoid nested calls,
63+
* increasing it here results in effectively disabling the tracing.
64+
*/
65+
public static class RunnerAdvice {
66+
@Advice.OnMethodEnter
67+
public static void beforeRunnerStart(@Advice.Argument(value = 1) String[] remoteArgs) {
68+
if (remoteArgs.length == 0) {
69+
CallDepthThreadLocalMap.incrementCallDepth(Reporter.class);
70+
}
71+
}
72+
73+
@Advice.OnMethodExit
74+
public static void afterRunnerStart(@Advice.Argument(value = 1) String[] remoteArgs) {
75+
if (remoteArgs.length == 0) {
76+
CallDepthThreadLocalMap.decrementCallDepth(Reporter.class);
77+
}
78+
}
79+
}
80+
}

dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public final class CiVisibilityConfig {
7979
public static final String CIVISIBILITY_REMOTE_ENV_VARS_PROVIDER_KEY =
8080
"civisibility.remote.env.vars.provider.key";
8181
public static final String CIVISIBILITY_TEST_ORDER = "civisibility.test.order";
82+
public static final String CIVISIBILITY_SCALATEST_FORK_MONITOR_ENABLED =
83+
"civisibility.scalatest.fork.monitor.enabled";
8284
public static final String TEST_MANAGEMENT_ENABLED = "test.management.enabled";
8385
public static final String TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES =
8486
"test.management.attempt.to.fix.retries";

internal-api/src/main/java/datadog/trace/api/Config.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ public static String getHostName() {
383383
private final String ciVisibilityTestOrder;
384384
private final boolean ciVisibilityTestManagementEnabled;
385385
private final Integer ciVisibilityTestManagementAttemptToFixRetries;
386+
private final boolean ciVisibilityScalatestForkMonitorEnabled;
386387

387388
private final boolean remoteConfigEnabled;
388389
private final boolean remoteConfigIntegrityCheckEnabled;
@@ -1607,6 +1608,8 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
16071608
ciVisibilityTestManagementEnabled = configProvider.getBoolean(TEST_MANAGEMENT_ENABLED, true);
16081609
ciVisibilityTestManagementAttemptToFixRetries =
16091610
configProvider.getInteger(TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES);
1611+
ciVisibilityScalatestForkMonitorEnabled =
1612+
configProvider.getBoolean(CIVISIBILITY_SCALATEST_FORK_MONITOR_ENABLED, false);
16101613

16111614
remoteConfigEnabled =
16121615
configProvider.getBoolean(
@@ -3114,6 +3117,10 @@ public boolean isCiVisibilityExecutionPoliciesEnabled() {
31143117
|| ciVisibilityTestManagementEnabled;
31153118
}
31163119

3120+
public boolean isCiVisibilityScalatestForkMonitorEnabled() {
3121+
return ciVisibilityScalatestForkMonitorEnabled;
3122+
}
3123+
31173124
public int getCiVisibilityFlakyRetryCount() {
31183125
return ciVisibilityFlakyRetryCount;
31193126
}

0 commit comments

Comments
 (0)