77import com .google .auto .service .AutoService ;
88import datadog .trace .agent .tooling .Instrumenter ;
99import datadog .trace .agent .tooling .InstrumenterModule ;
10+ import datadog .trace .bootstrap .CallDepthThreadLocalMap ;
1011import java .util .Set ;
1112import net .bytebuddy .asm .Advice ;
13+ import org .scalatest .Reporter ;
1214import org .scalatest .events .Event ;
1315
1416@ AutoService (InstrumenterModule .class )
1517public class ScalatestInstrumentation extends InstrumenterModule .CiVisibility
16- implements Instrumenter .ForSingleType , Instrumenter .HasMethodAdvice {
18+ implements Instrumenter .ForKnownTypes , Instrumenter .HasMethodAdvice {
1719
1820 public ScalatestInstrumentation () {
1921 super ("ci-visibility" , "scalatest" );
@@ -25,8 +27,10 @@ public boolean isApplicable(Set<TargetSystem> enabledSystems) {
2527 }
2628
2729 @ Override
28- public String instrumentedType () {
29- return "org.scalatest.DispatchReporter" ;
30+ public String [] knownMatchingTypes () {
31+ return new String [] {
32+ "org.scalatest.DispatchReporter" , "org.scalatest.tools.TestSortingReporter" ,
33+ };
3034 }
3135
3236 @ Override
@@ -46,13 +50,21 @@ public void methodAdvice(MethodTransformer transformer) {
4650 .and (takesArguments (1 ))
4751 .and (takesArgument (0 , named ("org.scalatest.events.Event" ))),
4852 ScalatestInstrumentation .class .getName () + "$DispatchEventAdvice" );
53+ transformer .applyAdvice (
54+ named ("fireReadyEvents" ),
55+ ScalatestInstrumentation .class .getName () + "$SuppressAsyncEventsAdvice" );
4956 }
5057
5158 public static class DispatchEventAdvice {
5259 @ Advice .OnMethodEnter
5360 public static void onDispatchEvent (@ Advice .Argument (value = 0 ) Event event ) {
61+ if (CallDepthThreadLocalMap .incrementCallDepth (Reporter .class ) != 0 ) {
62+ // nested call
63+ return ;
64+ }
65+
5466 // Instead of registering our reporter using Scalatest's standard "-C" argument,
55- // we hook into internal dispatch reporter.
67+ // we hook into internal reporter.
5668 // The reason is that Scalatest invokes registered reporters in a separate thread,
5769 // while we need to process events in the thread where they originate.
5870 // This is required because test span has to be active in the thread where
@@ -61,5 +73,28 @@ public static void onDispatchEvent(@Advice.Argument(value = 0) Event event) {
6173 // could be properly associated with it.
6274 DatadogReporter .handle (event );
6375 }
76+
77+ @ Advice .OnMethodExit
78+ public static void afterDispatchEvent () {
79+ CallDepthThreadLocalMap .decrementCallDepth (Reporter .class );
80+ }
81+ }
82+
83+ /**
84+ * {@link org.scalatest.tools.TestSortingReporter#fireReadyEvents} is triggered asynchronously. It
85+ * fires some events that are then delegated to other reporters. We need to suppress them (by
86+ * increasing the call depth so that {@link DispatchEventAdvice} is aborted) as the same events
87+ * are reported earlier synchronously from {@link org.scalatest.tools.TestSortingReporter#apply}
88+ */
89+ public static class SuppressAsyncEventsAdvice {
90+ @ Advice .OnMethodEnter
91+ public static void onAsyncEventsTrigger () {
92+ CallDepthThreadLocalMap .incrementCallDepth (Reporter .class );
93+ }
94+
95+ @ Advice .OnMethodExit
96+ public static void afterAsyncEventsTrigger () {
97+ CallDepthThreadLocalMap .decrementCallDepth (Reporter .class );
98+ }
6499 }
65100}
0 commit comments