diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index cce00af1d0d..875bcba4ba9 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -111,6 +111,7 @@
/dd-java-agent/instrumentation/jacoco-0.8.9/ @DataDog/ci-app-libraries
/dd-java-agent/instrumentation/junit @DataDog/ci-app-libraries
/dd-java-agent/instrumentation/karate-1.0/ @DataDog/ci-app-libraries
+/dd-java-agent/instrumentation/robolectric/ @DataDog/ci-app-libraries
/dd-java-agent/instrumentation/scalatest-3.0.8/ @DataDog/ci-app-libraries
/dd-java-agent/instrumentation/selenium-3.13/ @DataDog/ci-app-libraries
/dd-java-agent/instrumentation/testng/ @DataDog/ci-app-libraries
diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java
index 5266de922af..e8f4aaf9f69 100644
--- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java
+++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java
@@ -22,6 +22,7 @@
import datadog.trace.api.civisibility.telemetry.tag.EventType;
import datadog.trace.api.civisibility.telemetry.tag.FailedTestReplayEnabled;
import datadog.trace.api.civisibility.telemetry.tag.HasFailedAllRetries;
+import datadog.trace.api.civisibility.telemetry.tag.IsAndroidEmulated;
import datadog.trace.api.civisibility.telemetry.tag.IsAttemptToFix;
import datadog.trace.api.civisibility.telemetry.tag.IsDisabled;
import datadog.trace.api.civisibility.telemetry.tag.IsModified;
@@ -323,7 +324,8 @@ public void end(@Nullable Long endTime) {
span.getTag(Tags.TEST_IS_RUM_ACTIVE) != null ? IsRum.TRUE : null,
CIConstants.SELENIUM_BROWSER_DRIVER.equals(span.getTag(Tags.TEST_BROWSER_DRIVER))
? BrowserDriver.SELENIUM
- : null);
+ : null,
+ span.getTag(Tags.TEST_ANDROID_API_LEVEL) != null ? IsAndroidEmulated.TRUE : null);
}
/**
diff --git a/dd-java-agent/instrumentation/robolectric/robolectric-4.11/build.gradle b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/build.gradle
new file mode 100644
index 00000000000..a61b36a1397
--- /dev/null
+++ b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/build.gradle
@@ -0,0 +1,21 @@
+muzzle {
+ pass {
+ group = 'org.robolectric'
+ module = 'robolectric'
+ versions = '[4.11,)'
+ }
+}
+
+apply from: "$rootDir/gradle/java.gradle"
+
+dependencies {
+ // Instrumentation is validated through a scenario in `GradleDaemonSmokeTest`.
+ // Exclude androidx.test:monitor: it is an Android archive (.aar) published only to Google's Maven
+ // repository, is not referenced by the advice/helper.
+ compileOnly(group: 'org.robolectric', name: 'robolectric', version: '4.16.1') {
+ exclude group: 'androidx.test', module: 'monitor'
+ }
+ // RobolectricTestRunner extends JUnit's BlockJUnit4ClassRunner; JUnit must be on the compile
+ // classpath so its supertypes resolve (both javac and forbiddenApis walk the class hierarchy).
+ compileOnly group: 'junit', name: 'junit', version: '4.13.2'
+}
diff --git a/dd-java-agent/instrumentation/robolectric/robolectric-4.11/gradle.lockfile b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/gradle.lockfile
new file mode 100644
index 00000000000..8fdcd30dcc0
--- /dev/null
+++ b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/gradle.lockfile
@@ -0,0 +1,150 @@
+# This is a Gradle generated file for dependency locking.
+# Manual edits can break the build and are not advised.
+# This file is expected to be part of source control.
+# To regenerate this file, run: ./gradlew :dd-java-agent:instrumentation:robolectric:robolectric-4.11:dependencies --write-locks
+cafe.cryptography:curve25519-elisabeth:0.1.0=testRuntimeClasspath
+cafe.cryptography:ed25519-elisabeth:0.1.0=testRuntimeClasspath
+ch.qos.logback:logback-classic:1.2.13=testCompileClasspath,testRuntimeClasspath
+ch.qos.logback:logback-core:1.2.13=testCompileClasspath,testRuntimeClasspath
+com.almworks.sqlite4java:sqlite4java:1.0.392=compileClasspath
+com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+com.datadoghq.okhttp3:okhttp:3.12.15=testCompileClasspath,testRuntimeClasspath
+com.datadoghq.okio:okio:1.17.6=testCompileClasspath,testRuntimeClasspath
+com.datadoghq:dd-instrument-java:0.0.4=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+com.datadoghq:java-dogstatsd-client:4.4.5=testRuntimeClasspath
+com.datadoghq:sketches-java:0.8.3=testRuntimeClasspath
+com.github.javaparser:javaparser-core:3.25.6=codenarc
+com.github.jnr:jffi:1.3.15=testRuntimeClasspath
+com.github.jnr:jnr-a64asm:1.0.0=testRuntimeClasspath
+com.github.jnr:jnr-constants:0.10.4=testRuntimeClasspath
+com.github.jnr:jnr-enxio:0.32.20=testRuntimeClasspath
+com.github.jnr:jnr-ffi:2.2.19=testRuntimeClasspath
+com.github.jnr:jnr-posix:3.1.22=testRuntimeClasspath
+com.github.jnr:jnr-unixsocket:0.38.25=testRuntimeClasspath
+com.github.jnr:jnr-x86asm:1.0.2=testRuntimeClasspath
+com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,spotbugs
+com.github.spotbugs:spotbugs:4.9.8=spotbugs
+com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
+com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,testAnnotationProcessor,testCompileClasspath
+com.google.auto.service:auto-service:1.1.1=annotationProcessor,testAnnotationProcessor
+com.google.auto.value:auto-value-annotations:1.11.0=compileClasspath
+com.google.auto:auto-common:1.2.1=annotationProcessor,testAnnotationProcessor
+com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
+com.google.code.gson:gson:2.13.2=spotbugs
+com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,testAnnotationProcessor
+com.google.errorprone:error_prone_annotations:2.36.0=compileClasspath
+com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
+com.google.errorprone:error_prone_annotations:2.47.0=testCompileClasspath,testRuntimeClasspath
+com.google.guava:failureaccess:1.0.1=annotationProcessor,testAnnotationProcessor
+com.google.guava:failureaccess:1.0.3=compileClasspath,testCompileClasspath,testRuntimeClasspath
+com.google.guava:guava:32.0.1-jre=annotationProcessor,testAnnotationProcessor
+com.google.guava:guava:33.4.8-jre=compileClasspath
+com.google.guava:guava:33.6.0-jre=testCompileClasspath,testRuntimeClasspath
+com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,compileClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
+com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,testAnnotationProcessor
+com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath
+com.google.j2objc:j2objc-annotations:3.1=testCompileClasspath,testRuntimeClasspath
+com.google.re2j:re2j:1.8=testRuntimeClasspath
+com.ibm.icu:icu4j:77.1=compileClasspath
+com.squareup.moshi:moshi:1.11.0=testCompileClasspath,testRuntimeClasspath
+com.squareup.okhttp3:logging-interceptor:3.12.12=testCompileClasspath,testRuntimeClasspath
+com.squareup.okhttp3:okhttp:3.12.12=testCompileClasspath,testRuntimeClasspath
+com.squareup.okio:okio:1.17.5=testCompileClasspath,testRuntimeClasspath
+com.thoughtworks.qdox:qdox:1.12.1=codenarc
+commons-fileupload:commons-fileupload:1.5=testCompileClasspath,testRuntimeClasspath
+commons-io:commons-io:2.11.0=testCompileClasspath,testRuntimeClasspath
+commons-io:commons-io:2.20.0=spotbugs
+de.thetaphi:forbiddenapis:3.10=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath
+io.sqreen:libsqreen:17.3.0=testRuntimeClasspath
+javax.annotation:javax.annotation-api:1.3.2=compileClasspath
+javax.inject:javax.inject:1=compileClasspath
+javax.servlet:javax.servlet-api:3.1.0=testCompileClasspath,testRuntimeClasspath
+jaxen:jaxen:2.0.0=spotbugs
+junit:junit:4.13.2=compileClasspath,testRuntimeClasspath
+net.bytebuddy:byte-buddy-agent:1.18.10=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+net.bytebuddy:byte-buddy:1.18.10=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+net.java.dev.jna:jna-platform:5.8.0=testRuntimeClasspath
+net.java.dev.jna:jna:5.8.0=testRuntimeClasspath
+net.sf.saxon:Saxon-HE:12.9=spotbugs
+org.apache.ant:ant-antlr:1.10.14=codenarc
+org.apache.ant:ant-junit:1.10.14=codenarc
+org.apache.bcel:bcel:6.11.0=spotbugs
+org.apache.commons:commons-lang3:3.19.0=spotbugs
+org.apache.commons:commons-text:1.14.0=spotbugs
+org.apache.logging.log4j:log4j-api:2.25.2=spotbugs
+org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
+org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testRuntimeClasspath
+org.bouncycastle:bcprov-jdk18on:1.81=compileClasspath
+org.checkerframework:checker-qual:3.33.0=annotationProcessor,testAnnotationProcessor
+org.codehaus.groovy:groovy-ant:3.0.23=codenarc
+org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc
+org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc
+org.codehaus.groovy:groovy-json:3.0.23=codenarc
+org.codehaus.groovy:groovy-json:3.0.25=testCompileClasspath,testRuntimeClasspath
+org.codehaus.groovy:groovy-templates:3.0.23=codenarc
+org.codehaus.groovy:groovy-xml:3.0.23=codenarc
+org.codehaus.groovy:groovy:3.0.23=codenarc
+org.codehaus.groovy:groovy:3.0.25=testCompileClasspath,testRuntimeClasspath
+org.codenarc:CodeNarc:3.7.0=codenarc
+org.dom4j:dom4j:2.2.0=spotbugs
+org.gmetrics:GMetrics:2.1.0=codenarc
+org.hamcrest:hamcrest-core:1.3=compileClasspath,testRuntimeClasspath
+org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath
+org.jctools:jctools-core-jdk11:4.0.6=testRuntimeClasspath
+org.jctools:jctools-core:4.0.6=testRuntimeClasspath
+org.jspecify:jspecify:1.0.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-launcher:1.14.1=testRuntimeClasspath
+org.junit.platform:junit-platform-runner:1.14.1=testRuntimeClasspath
+org.junit.platform:junit-platform-suite-api:1.14.1=testRuntimeClasspath
+org.junit.platform:junit-platform-suite-commons:1.14.1=testRuntimeClasspath
+org.junit:junit-bom:5.14.0=spotbugs
+org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath
+org.mockito:mockito-core:4.4.0=testRuntimeClasspath
+org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath
+org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
+org.ow2.asm:asm-analysis:9.7.1=testRuntimeClasspath
+org.ow2.asm:asm-analysis:9.9=spotbugs
+org.ow2.asm:asm-commons:9.8=compileClasspath
+org.ow2.asm:asm-commons:9.9=spotbugs
+org.ow2.asm:asm-commons:9.9.1=testRuntimeClasspath
+org.ow2.asm:asm-tree:9.8=compileClasspath
+org.ow2.asm:asm-tree:9.9=spotbugs
+org.ow2.asm:asm-tree:9.9.1=testRuntimeClasspath
+org.ow2.asm:asm-util:9.7.1=testRuntimeClasspath
+org.ow2.asm:asm-util:9.9=spotbugs
+org.ow2.asm:asm:9.9=spotbugs
+org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.robolectric:annotations:4.16.1=compileClasspath
+org.robolectric:junit:4.16.1=compileClasspath
+org.robolectric:nativeruntime:4.16.1=compileClasspath
+org.robolectric:pluginapi:4.16.1=compileClasspath
+org.robolectric:plugins-maven-dependency-resolver:4.16.1=compileClasspath
+org.robolectric:resources:4.16.1=compileClasspath
+org.robolectric:robolectric:4.16.1=compileClasspath
+org.robolectric:sandbox:4.16.1=compileClasspath
+org.robolectric:shadowapi:4.16.1=compileClasspath
+org.robolectric:shadows-framework:4.16.1=compileClasspath
+org.robolectric:utils-reflector:4.16.1=compileClasspath
+org.robolectric:utils:4.16.1=compileClasspath
+org.slf4j:jcl-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath
+org.slf4j:jul-to-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath
+org.slf4j:log4j-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath
+org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath
+org.slf4j:slf4j-api:1.7.32=testCompileClasspath,testRuntimeClasspath
+org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j
+org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
+org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,muzzleTooling,runtimeClasspath,testRuntimeClasspath
+org.spockframework:spock-bom:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath
+org.spockframework:spock-core:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath
+org.tabletest:tabletest-junit:1.2.1=testCompileClasspath,testRuntimeClasspath
+org.tabletest:tabletest-parser:1.2.0=testCompileClasspath,testRuntimeClasspath
+org.xmlresolver:xmlresolver:5.3.3=spotbugs
+empty=spotbugsPlugins
diff --git a/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricInstrumentation.java b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricInstrumentation.java
new file mode 100644
index 00000000000..f38922993d3
--- /dev/null
+++ b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricInstrumentation.java
@@ -0,0 +1,55 @@
+package datadog.trace.instrumentation.robolectric;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.InstrumenterModule;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+/**
+ * Captures the emulated Android SDK for tests running under Robolectric.
+ *
+ *
Robolectric establishes the emulated SDK in {@code TestEnvironment#setUpApplicationState},
+ * which runs on the per-SDK sandbox "main" thread right before the test body (the SDK is not yet
+ * set when the JUnit test-start event fires, and is torn down before the finish event).
+ */
+@AutoService(InstrumenterModule.class)
+public class RobolectricInstrumentation extends InstrumenterModule.CiVisibility
+ implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
+
+ public RobolectricInstrumentation() {
+ super("ci-visibility", "robolectric");
+ }
+
+ @Override
+ public String hierarchyMarkerType() {
+ return "org.robolectric.internal.TestEnvironment";
+ }
+
+ @Override
+ public ElementMatcher hierarchyMatcher() {
+ return implementsInterface(named(hierarchyMarkerType()));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {packageName + ".RobolectricTestAnnotator"};
+ }
+
+ @Override
+ public void methodAdvice(MethodTransformer transformer) {
+ transformer.applyAdvice(
+ named("setUpApplicationState"), getClass().getName() + "$SetUpApplicationStateAdvice");
+ }
+
+ public static class SetUpApplicationStateAdvice {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void onExit() {
+ RobolectricTestAnnotator.annotate();
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricTestAnnotator.java b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricTestAnnotator.java
new file mode 100644
index 00000000000..8396ddc8c2d
--- /dev/null
+++ b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricTestAnnotator.java
@@ -0,0 +1,71 @@
+package datadog.trace.instrumentation.robolectric;
+
+import datadog.trace.api.gateway.RequestContext;
+import datadog.trace.api.gateway.RequestContextSlot;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
+import datadog.trace.bootstrap.instrumentation.api.Tags;
+import java.io.File;
+import java.net.URL;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.versioning.AndroidVersions;
+
+public final class RobolectricTestAnnotator {
+
+ /** Matches the version in a {@code robolectric-.jar} file name. */
+ private static final Pattern ROBOLECTRIC_JAR = Pattern.compile("^robolectric-(.+)\\.jar$");
+
+ private RobolectricTestAnnotator() {}
+
+ public static void annotate() {
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ if (apiLevel <= 0) {
+ return;
+ }
+
+ AgentSpan span = AgentTracer.activeSpan();
+ if (span == null) {
+ return;
+ }
+ RequestContext requestContext = span.getRequestContext();
+ if (requestContext == null
+ || requestContext.getData(RequestContextSlot.CI_VISIBILITY) == null) {
+ // The active span is not a CI Visibility test span; nothing to enrich.
+ return;
+ }
+
+ span.setTag(Tags.TEST_ANDROID_API_LEVEL, apiLevel);
+ AndroidVersions.AndroidRelease release = AndroidVersions.getReleaseForSdkInt(apiLevel);
+ if (release != null) {
+ span.setTag(Tags.TEST_ANDROID_RELEASE, release.getVersion());
+ span.setTag(Tags.TEST_ANDROID_CODENAME, release.getShortCode());
+ }
+ String robolectricVersion = robolectricVersion();
+ if (robolectricVersion != null) {
+ span.setTag(Tags.TEST_ANDROID_ROBOLECTRIC_VERSION, robolectricVersion);
+ }
+ }
+
+ private static String robolectricVersion() {
+ try {
+ // RuntimeEnvironment is re-loaded by the sandbox classloader with no CodeSource, but the
+ // runner runs outside the sandbox (it creates it), so it is delegated to the application
+ // classloader and its CodeSource points at the real robolectric-.jar.
+ ProtectionDomain protectionDomain = RobolectricTestRunner.class.getProtectionDomain();
+ CodeSource codeSource = protectionDomain != null ? protectionDomain.getCodeSource() : null;
+ URL location = codeSource != null ? codeSource.getLocation() : null;
+ if (location == null) {
+ return null;
+ }
+ Matcher matcher = ROBOLECTRIC_JAR.matcher(new File(location.getPath()).getName());
+ return matcher.matches() ? matcher.group(1) : null;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+}
diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java
index 5461ec2056b..a4fdaa81aa0 100644
--- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java
+++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java
@@ -127,6 +127,35 @@ void testNew(
expectedCoverages);
}
+ @TableTest({
+ "scenario | gradleVersion | projectName | expectedTraces",
+ "robolectric-latest | latest | test-succeed-robolectric | 6 "
+ })
+ @ParameterizedTest
+ void testRobolectric(String gradleVersion, String projectName, int expectedTraces)
+ throws IOException {
+ // Robolectric 4.16's sandbox supports JDK 17-21
+ Assumptions.assumeTrue(
+ JavaVirtualMachine.isJavaVersionBetween(17, 22), "Robolectric 4.16 supports JDK 17-21");
+
+ gradleVersion = resolveVersion(gradleVersion);
+ givenGradleVersionIsCompatibleWithCurrentJvm(gradleVersion);
+ givenGradleVersionIsSupportedByCurrentGradleTestKit(gradleVersion);
+ givenGradleProjectFiles(projectName);
+ givenGradleProjectProperties();
+ ensureDependenciesDownloaded(gradleVersion);
+
+ BuildResult buildResult = runGradleTests(gradleVersion, true, false);
+ assertBuildSuccessful(buildResult);
+
+ verifyEventsAndCoverages(
+ projectName,
+ "gradle",
+ gradleVersion,
+ mockBackend.waitForEvents(expectedTraces),
+ mockBackend.waitForCoverages(0));
+ }
+
// TODO: add back LATEST_GRADLE_VERSION after fixing ordering on Gradle 9.3.0
@TableTest({
"scenario | gradleVersion | projectName | flakyTests | expectedOrder | eventsNumber",
diff --git a/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/build.gradleTest b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/build.gradleTest
new file mode 100644
index 00000000000..43e1b6068d5
--- /dev/null
+++ b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/build.gradleTest
@@ -0,0 +1,108 @@
+import java.util.zip.ZipFile
+import org.gradle.api.artifacts.transform.InputArtifact
+import org.gradle.api.artifacts.transform.TransformAction
+import org.gradle.api.artifacts.transform.TransformOutputs
+import org.gradle.api.artifacts.transform.TransformParameters
+import org.gradle.api.attributes.AttributeCompatibilityRule
+import org.gradle.api.attributes.CompatibilityCheckDetails
+import org.gradle.api.attributes.LibraryElements
+
+apply plugin: 'java'
+
+repositories {
+ mavenLocal()
+
+ def proxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY")
+ if (proxyUrl) {
+ println "Using proxy repository: $proxyUrl"
+ maven {
+ url = proxyUrl
+ allowInsecureProtocol = true
+ }
+ }
+
+ mavenCentral()
+
+ // androidx.test artifacts (Robolectric runtime dependencies) are published only to Google's Maven
+ // repository. Scope it strictly to androidx.* so nothing else resolves from it.
+ maven {
+ url = 'https://maven.google.com'
+ content {
+ includeGroupByRegex 'androidx\\..*'
+ }
+ }
+}
+
+// androidx.test ships as Android archives (.aar), which a plain-JVM build cannot consume directly.
+// Teach Gradle to accept the aar variant and extract its classes.jar — the part the Android Gradle
+// plugin would otherwise do. Safe to apply broadly here: this is a standalone project with no
+// class-directory (project) dependencies.
+abstract class AarToJarCompatibility implements AttributeCompatibilityRule {
+ @Override
+ void execute(CompatibilityCheckDetails details) {
+ if (details.producerValue != null && details.producerValue.name == 'aar') {
+ details.compatible()
+ }
+ }
+}
+
+abstract class ExtractAarClasses implements TransformAction {
+ @InputArtifact
+ abstract Provider getInputArtifact()
+
+ @Override
+ void transform(TransformOutputs outputs) {
+ def aar = inputArtifact.get().asFile
+ if (!aar.name.endsWith('.aar')) {
+ outputs.file(aar)
+ return
+ }
+ new ZipFile(aar).withCloseable { zip ->
+ def entry = zip.getEntry('classes.jar')
+ def out = outputs.file(aar.name - '.aar' + '.jar')
+ if (entry != null) {
+ out.withOutputStream { os -> zip.getInputStream(entry).withCloseable { input -> os << input } }
+ } else {
+ new java.util.jar.JarOutputStream(out.newOutputStream()).close()
+ }
+ }
+ }
+}
+
+def artifactType = Attribute.of('artifactType', String)
+
+dependencies {
+ attributesSchema {
+ attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
+ compatibilityRules.add(AarToJarCompatibility)
+ }
+ }
+ registerTransform(ExtractAarClasses) {
+ from.attribute(artifactType, 'aar')
+ to.attribute(artifactType, 'jar')
+ }
+}
+
+configurations.configureEach {
+ if (canBeResolved) {
+ attributes.attribute(artifactType, 'jar')
+ }
+}
+
+dependencies {
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.robolectric:robolectric:4.16.1'
+ // Pre-built Android SDK jar for the level the fixtures configure.
+ testImplementation 'org.robolectric:android-all:14-robolectric-10818077'
+ // androidx.test:core pulls in androidx.test:monitor (InstrumentationRegistry); ext:junit provides
+ // the AndroidJUnit4 runner, which delegates to RobolectricTestRunner for local (host-JVM) tests.
+ testImplementation 'androidx.test:core:1.6.1'
+ testImplementation 'androidx.test.ext:junit:1.2.1'
+}
+
+test {
+ useJUnit()
+ // Coverage is validated by the other Gradle smoke scenarios; disable it here so this scenario
+ // stays focused on the emulated-SDK (test.android.*) tags.
+ environment "DD_CIVISIBILITY_CODE_COVERAGE_ENABLED", "false"
+}
diff --git a/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/coverages.ftl b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/coverages.ftl
new file mode 100644
index 00000000000..8878e547a79
--- /dev/null
+++ b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/coverages.ftl
@@ -0,0 +1 @@
+[ ]
\ No newline at end of file
diff --git a/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/events.ftl b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/events.ftl
new file mode 100644
index 00000000000..d01993dbd3d
--- /dev/null
+++ b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/events.ftl
@@ -0,0 +1,502 @@
+[ {
+ "content" : {
+ "duration" : ${content_duration},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid},
+ "_dd.test.is_user_provided_service" : "true",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "ci.workspace_path" : ${content_meta_ci_workspace_path},
+ "component" : "gradle",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version},
+ "span.kind" : "test_session_end",
+ "test.code_coverage.enabled" : "true",
+ "test.command" : "gradle test",
+ "test.framework" : "junit4",
+ "test.framework_version" : "4.13.2",
+ "test.itr.tests_skipping.enabled" : "true",
+ "test.itr.tests_skipping.type" : "test",
+ "test.status" : "pass",
+ "test.toolchain" : ${content_meta_test_toolchain},
+ "test.type" : "test",
+ "test_session.name" : "gradle test"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id},
+ "test.itr.tests_skipping.count" : 0
+ },
+ "name" : "gradle.test_session",
+ "resource" : ":",
+ "service" : "test-gradle-service",
+ "start" : ${content_start},
+ "test_session_id" : ${content_test_session_id}
+ },
+ "type" : "test_session_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_2},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_2},
+ "_dd.test.is_user_provided_service" : "true",
+ "ci.workspace_path" : ${content_meta_ci_workspace_path},
+ "component" : "gradle",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version},
+ "span.kind" : "test_module_end",
+ "test.code_coverage.enabled" : "true",
+ "test.command" : "gradle test",
+ "test.framework" : "junit4",
+ "test.framework_version" : "4.13.2",
+ "test.itr.tests_skipping.enabled" : "true",
+ "test.itr.tests_skipping.type" : "test",
+ "test.module" : ":test",
+ "test.status" : "pass",
+ "test.type" : "test",
+ "test_session.name" : "gradle test"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2},
+ "test.itr.tests_skipping.count" : 0
+ },
+ "name" : "gradle.test_module",
+ "resource" : ":test",
+ "service" : "test-gradle-service",
+ "start" : ${content_start_2},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id}
+ },
+ "type" : "test_module_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_3},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_3},
+ "_dd.test.is_user_provided_service" : "true",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version}
+ },
+ "metrics" : { },
+ "name" : "classes",
+ "parent_id" : ${content_test_session_id},
+ "resource" : "classes",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id},
+ "start" : ${content_start_3},
+ "trace_id" : ${content_test_session_id}
+ },
+ "type" : "span",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_4},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_4},
+ "_dd.test.is_user_provided_service" : "true",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version}
+ },
+ "metrics" : { },
+ "name" : "compileJava",
+ "parent_id" : ${content_test_session_id},
+ "resource" : "compileJava",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_2},
+ "start" : ${content_start_4},
+ "trace_id" : ${content_test_session_id}
+ },
+ "type" : "span",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_5},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_5},
+ "_dd.test.is_user_provided_service" : "true",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version}
+ },
+ "metrics" : { },
+ "name" : "compileTestJava",
+ "parent_id" : ${content_test_session_id},
+ "resource" : "compileTestJava",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_3},
+ "start" : ${content_start_5},
+ "trace_id" : ${content_test_session_id}
+ },
+ "type" : "span",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_6},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_6},
+ "_dd.test.is_user_provided_service" : "true",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "ci.workspace_path" : ${content_meta_ci_workspace_path},
+ "component" : "junit4",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id_2},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version},
+ "span.kind" : "test_suite_end",
+ "test.framework" : "junit4",
+ "test.framework_version" : "4.13.2",
+ "test.itr.tests_skipping.enabled" : "true",
+ "test.module" : ":test",
+ "test.source.file" : "src/test/java/datadog/smoke/AndroidJUnit4RunnerTest.java",
+ "test.status" : "pass",
+ "test.suite" : "datadog.smoke.AndroidJUnit4RunnerTest",
+ "test.type" : "test",
+ "test_session.name" : "gradle test"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id_2},
+ "test.source.end" : 19,
+ "test.source.start" : 11
+ },
+ "name" : "junit4.test_suite",
+ "resource" : "datadog.smoke.AndroidJUnit4RunnerTest",
+ "service" : "test-gradle-service",
+ "start" : ${content_start_6},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id}
+ },
+ "type" : "test_suite_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_7},
+ "error" : 0,
+ "meta" : {
+ "_dd.library_capabilities.auto_test_retries" : "1",
+ "_dd.library_capabilities.coverage_report_upload" : "1",
+ "_dd.library_capabilities.early_flake_detection" : "1",
+ "_dd.library_capabilities.fail_fast_test_order" : "1",
+ "_dd.library_capabilities.failed_test_replay" : "1",
+ "_dd.library_capabilities.impacted_tests" : "1",
+ "_dd.library_capabilities.test_impact_analysis" : "1",
+ "_dd.library_capabilities.test_management.attempt_to_fix" : "5",
+ "_dd.library_capabilities.test_management.disable" : "1",
+ "_dd.library_capabilities.test_management.quarantine" : "1",
+ "_dd.p.tid" : ${content_meta__dd_p_tid_7},
+ "_dd.test.is_user_provided_service" : "true",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "ci.workspace_path" : ${content_meta_ci_workspace_path},
+ "component" : "junit4",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id_2},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version},
+ "span.kind" : "test",
+ "test.android.codename" : "U",
+ "test.android.release" : "14",
+ "test.android.robolectric.version" : "4.16.1",
+ "test.final_status" : "pass",
+ "test.framework" : "junit4",
+ "test.framework_version" : "4.13.2",
+ "test.itr.tests_skipping.enabled" : "true",
+ "test.module" : ":test",
+ "test.name" : "test_androidjunit4_runner",
+ "test.source.file" : "src/test/java/datadog/smoke/AndroidJUnit4RunnerTest.java",
+ "test.source.method" : "test_androidjunit4_runner()V",
+ "test.status" : "pass",
+ "test.suite" : "datadog.smoke.AndroidJUnit4RunnerTest",
+ "test.type" : "test",
+ "test_session.name" : "gradle test"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id_2},
+ "test.android.api_level" : 34,
+ "test.source.end" : 18,
+ "test.source.start" : 15
+ },
+ "name" : "junit4.test",
+ "parent_id" : ${content_parent_id},
+ "resource" : "datadog.smoke.AndroidJUnit4RunnerTest.test_androidjunit4_runner",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_4},
+ "start" : ${content_start_7},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id},
+ "trace_id" : ${content_trace_id}
+ },
+ "type" : "test",
+ "version" : 2
+}, {
+ "content" : {
+ "duration" : ${content_duration_8},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_8},
+ "_dd.test.is_user_provided_service" : "true",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "ci.workspace_path" : ${content_meta_ci_workspace_path},
+ "component" : "junit4",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id_2},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version},
+ "span.kind" : "test_suite_end",
+ "test.framework" : "junit4",
+ "test.framework_version" : "4.13.2",
+ "test.itr.tests_skipping.enabled" : "true",
+ "test.module" : ":test",
+ "test.source.file" : "src/test/java/datadog/smoke/RobolectricRunnerTest.java",
+ "test.status" : "pass",
+ "test.suite" : "datadog.smoke.RobolectricRunnerTest",
+ "test.type" : "test",
+ "test_session.name" : "gradle test"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_5},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id_2},
+ "test.source.end" : 19,
+ "test.source.start" : 11
+ },
+ "name" : "junit4.test_suite",
+ "resource" : "datadog.smoke.RobolectricRunnerTest",
+ "service" : "test-gradle-service",
+ "start" : ${content_start_8},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id_2}
+ },
+ "type" : "test_suite_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_9},
+ "error" : 0,
+ "meta" : {
+ "_dd.library_capabilities.auto_test_retries" : "1",
+ "_dd.library_capabilities.coverage_report_upload" : "1",
+ "_dd.library_capabilities.early_flake_detection" : "1",
+ "_dd.library_capabilities.fail_fast_test_order" : "1",
+ "_dd.library_capabilities.failed_test_replay" : "1",
+ "_dd.library_capabilities.impacted_tests" : "1",
+ "_dd.library_capabilities.test_impact_analysis" : "1",
+ "_dd.library_capabilities.test_management.attempt_to_fix" : "5",
+ "_dd.library_capabilities.test_management.disable" : "1",
+ "_dd.library_capabilities.test_management.quarantine" : "1",
+ "_dd.p.tid" : ${content_meta__dd_p_tid_9},
+ "_dd.test.is_user_provided_service" : "true",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "ci.workspace_path" : ${content_meta_ci_workspace_path},
+ "component" : "junit4",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id_2},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version},
+ "span.kind" : "test",
+ "test.android.codename" : "U",
+ "test.android.release" : "14",
+ "test.android.robolectric.version" : "4.16.1",
+ "test.final_status" : "pass",
+ "test.framework" : "junit4",
+ "test.framework_version" : "4.13.2",
+ "test.itr.tests_skipping.enabled" : "true",
+ "test.module" : ":test",
+ "test.name" : "test_robolectric_runner",
+ "test.source.file" : "src/test/java/datadog/smoke/RobolectricRunnerTest.java",
+ "test.source.method" : "test_robolectric_runner()V",
+ "test.status" : "pass",
+ "test.suite" : "datadog.smoke.RobolectricRunnerTest",
+ "test.type" : "test",
+ "test_session.name" : "gradle test"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_6},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id_2},
+ "test.android.api_level" : 34,
+ "test.source.end" : 18,
+ "test.source.start" : 15
+ },
+ "name" : "junit4.test",
+ "parent_id" : ${content_parent_id},
+ "resource" : "datadog.smoke.RobolectricRunnerTest.test_robolectric_runner",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_5},
+ "start" : ${content_start_9},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id_2},
+ "trace_id" : ${content_trace_id_2}
+ },
+ "type" : "test",
+ "version" : 2
+}, {
+ "content" : {
+ "duration" : ${content_duration_10},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_10},
+ "_dd.test.is_user_provided_service" : "true",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version}
+ },
+ "metrics" : { },
+ "name" : "processResources",
+ "parent_id" : ${content_test_session_id},
+ "resource" : "processResources",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_6},
+ "start" : ${content_start_10},
+ "trace_id" : ${content_test_session_id}
+ },
+ "type" : "span",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_11},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_11},
+ "_dd.test.is_user_provided_service" : "true",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version}
+ },
+ "metrics" : { },
+ "name" : "processTestResources",
+ "parent_id" : ${content_test_session_id},
+ "resource" : "processTestResources",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_7},
+ "start" : ${content_start_11},
+ "trace_id" : ${content_test_session_id}
+ },
+ "type" : "span",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_12},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_12},
+ "_dd.test.is_user_provided_service" : "true",
+ "env" : "integration-test",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "os.architecture" : ${content_meta_os_architecture},
+ "os.platform" : ${content_meta_os_platform},
+ "os.version" : ${content_meta_os_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "runtime.name" : ${content_meta_runtime_name},
+ "runtime.vendor" : ${content_meta_runtime_vendor},
+ "runtime.version" : ${content_meta_runtime_version}
+ },
+ "metrics" : { },
+ "name" : "testClasses",
+ "parent_id" : ${content_test_session_id},
+ "resource" : "testClasses",
+ "service" : "test-gradle-service",
+ "span_id" : ${content_span_id_8},
+ "start" : ${content_start_12},
+ "trace_id" : ${content_test_session_id}
+ },
+ "type" : "span",
+ "version" : 1
+} ]
\ No newline at end of file
diff --git a/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/settings.gradleTest b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/settings.gradleTest
new file mode 100644
index 00000000000..6040f1decb8
--- /dev/null
+++ b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/settings.gradleTest
@@ -0,0 +1 @@
+rootProject.name = 'gradle-instrumentation-test-project'
diff --git a/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/src/test/java/datadog/smoke/AndroidJUnit4RunnerTest.java b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/src/test/java/datadog/smoke/AndroidJUnit4RunnerTest.java
new file mode 100644
index 00000000000..a85709853b1
--- /dev/null
+++ b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/src/test/java/datadog/smoke/AndroidJUnit4RunnerTest.java
@@ -0,0 +1,19 @@
+package datadog.smoke;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(sdk = 34, manifest = Config.NONE)
+public class AndroidJUnit4RunnerTest {
+
+ @Test
+ public void test_androidjunit4_runner() {
+ assertEquals(34, RuntimeEnvironment.getApiLevel());
+ }
+}
diff --git a/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/src/test/java/datadog/smoke/RobolectricRunnerTest.java b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/src/test/java/datadog/smoke/RobolectricRunnerTest.java
new file mode 100644
index 00000000000..d034022dd5d
--- /dev/null
+++ b/dd-smoke-tests/gradle/src/test/resources/test-succeed-robolectric/src/test/java/datadog/smoke/RobolectricRunnerTest.java
@@ -0,0 +1,19 @@
+package datadog.smoke;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 34, manifest = Config.NONE)
+public class RobolectricRunnerTest {
+
+ @Test
+ public void test_robolectric_runner() {
+ assertEquals(34, RuntimeEnvironment.getApiLevel());
+ }
+}
diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java
index 2f4336c3711..cbb8441e920 100644
--- a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java
+++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java
@@ -22,6 +22,7 @@
import datadog.trace.api.civisibility.telemetry.tag.HasCodeowner;
import datadog.trace.api.civisibility.telemetry.tag.HasFailedAllRetries;
import datadog.trace.api.civisibility.telemetry.tag.ImpactedTestsDetectionEnabled;
+import datadog.trace.api.civisibility.telemetry.tag.IsAndroidEmulated;
import datadog.trace.api.civisibility.telemetry.tag.IsAttemptToFix;
import datadog.trace.api.civisibility.telemetry.tag.IsDisabled;
import datadog.trace.api.civisibility.telemetry.tag.IsHeadless;
@@ -88,7 +89,8 @@ public enum CiVisibilityCountMetric {
RetryReason.class,
FailedTestReplayEnabled.TestMetric.class,
IsRum.class,
- BrowserDriver.class),
+ BrowserDriver.class,
+ IsAndroidEmulated.class),
/** The number of successfully collected code coverages that are empty */
CODE_COVERAGE_IS_EMPTY("code_coverage.is_empty"),
/** The number of errors while processing code coverage */
diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsAndroidEmulated.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsAndroidEmulated.java
new file mode 100644
index 00000000000..dbf61580911
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsAndroidEmulated.java
@@ -0,0 +1,13 @@
+package datadog.trace.api.civisibility.telemetry.tag;
+
+import datadog.trace.api.civisibility.telemetry.TagValue;
+
+/** Whether a test ran against an emulated Android SDK (e.g. under Robolectric). */
+public enum IsAndroidEmulated implements TagValue {
+ TRUE;
+
+ @Override
+ public String asString() {
+ return "is_android_emulated:true";
+ }
+}
diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java
index 14496e8b243..968d6eea3b3 100644
--- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java
+++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java
@@ -85,6 +85,13 @@ public class Tags {
public static final String TEST_BROWSER_NAME = "test.browser.name";
public static final String TEST_BROWSER_VERSION = "test.browser.version";
public static final String TEST_CALLBACK = "test.callback";
+ // Emulated Android SDK metadata. Robolectric runs tests on the host JVM against an emulated SDK,
+ // so the host os.*/runtime.* tags do not describe it. The SDK tags are per-test, since
+ // @Config(sdk=...) can vary per method.
+ public static final String TEST_ANDROID_API_LEVEL = "test.android.api_level";
+ public static final String TEST_ANDROID_RELEASE = "test.android.release";
+ public static final String TEST_ANDROID_CODENAME = "test.android.codename";
+ public static final String TEST_ANDROID_ROBOLECTRIC_VERSION = "test.android.robolectric.version";
public static final String TEST_SESSION_ID = "test_session_id";
public static final String TEST_MODULE_ID = "test_module_id";
diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json
index 733f3736cbb..389b5b1392f 100644
--- a/metadata/supported-configurations.json
+++ b/metadata/supported-configurations.json
@@ -9561,6 +9561,14 @@
"aliases": ["DD_TRACE_INTEGRATION_RMI_ENABLED", "DD_INTEGRATION_RMI_ENABLED"]
}
],
+ "DD_TRACE_ROBOLECTRIC_ENABLED": [
+ {
+ "version": "A",
+ "type": "boolean",
+ "default": "true",
+ "aliases": ["DD_TRACE_INTEGRATION_ROBOLECTRIC_ENABLED", "DD_INTEGRATION_ROBOLECTRIC_ENABLED"]
+ }
+ ],
"DD_TRACE_RMI_SERVER_ANALYTICS_ENABLED": [
{
"version": "A",
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 36b1d9b95df..d4a5b3077d7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -540,6 +540,7 @@ include(
":dd-java-agent:instrumentation:reactor-core-3.1",
":dd-java-agent:instrumentation:reactor-netty-1.0",
":dd-java-agent:instrumentation:rediscala-1.8",
+ ":dd-java-agent:instrumentation:robolectric:robolectric-4.11",
":dd-java-agent:instrumentation:redisson:redisson-2.0.0",
":dd-java-agent:instrumentation:redisson:redisson-2.3.0",
":dd-java-agent:instrumentation:redisson:redisson-3.10.3",