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..b91c9756cd3 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.IsAndroid; 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 ? IsAndroid.TRUE : null); } /** diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java index 26b083f0c17..2f0eec1c39a 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java @@ -5,6 +5,8 @@ import datadog.trace.api.civisibility.CIConstants; import datadog.trace.api.civisibility.DDTest; import datadog.trace.api.civisibility.DDTestSuite; +import datadog.trace.api.civisibility.android.AndroidTestContext; +import datadog.trace.api.civisibility.android.AndroidTestInfo; import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestEventsHandler; @@ -220,6 +222,24 @@ public void onTestStart( inProgressTests.put(descriptor, test); } + /** Applies emulated Android SDK metadata captured by the Robolectric instrumentation. */ + private void populateAndroidTags(TestImpl test) { + AndroidTestInfo androidInfo = AndroidTestContext.getAndClear(); + if (androidInfo == null) { + return; + } + test.setTag(Tags.TEST_ANDROID_API_LEVEL, androidInfo.getApiLevel()); + if (androidInfo.getRelease() != null) { + test.setTag(Tags.TEST_ANDROID_RELEASE, androidInfo.getRelease()); + } + if (androidInfo.getCodename() != null) { + test.setTag(Tags.TEST_ANDROID_CODENAME, androidInfo.getCodename()); + } + if (androidInfo.getRobolectricVersion() != null) { + test.setTag(Tags.TEST_ANDROID_ROBOLECTRIC_VERSION, androidInfo.getRobolectricVersion()); + } + } + @Override public void onTestSkip(TestKey descriptor, @Nullable String reason) { TestImpl test = inProgressTests.get(descriptor); @@ -284,6 +304,8 @@ public void onTestFinish( test.setTag(Tags.TEST_FINAL_STATUS, testStatus); } + populateAndroidTags(test); + test.end(endTime); } 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..984ee7bf45c --- /dev/null +++ b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricInstrumentation.java @@ -0,0 +1,63 @@ +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). This + * instrumentation reads the SDK there and stashes it in {@link + * datadog.trace.api.civisibility.android.AndroidTestContext}; the CI Visibility core drains it and + * attaches the {@code test.android.*} tags to the test span (see {@code + * TestEventsHandlerImpl#onTestFinish}). + * + *

Robolectric tests are JUnit tests, so their spans are still produced by the JUnit + * instrumentation — this only enriches them with the Android metadata, and only loads when + * Robolectric is on the classpath. + */ +@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 + ".RobolectricTestExtractor"}; + } + + @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() { + RobolectricTestExtractor.capture(); + } + } +} diff --git a/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricTestExtractor.java b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricTestExtractor.java new file mode 100644 index 00000000000..be42915300d --- /dev/null +++ b/dd-java-agent/instrumentation/robolectric/robolectric-4.11/src/main/java/datadog/trace/instrumentation/robolectric/RobolectricTestExtractor.java @@ -0,0 +1,58 @@ +package datadog.trace.instrumentation.robolectric; + +import datadog.trace.api.civisibility.android.AndroidTestContext; +import datadog.trace.api.civisibility.android.AndroidTestInfo; +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; + +/** + * Reads the emulated Android SDK and Robolectric version (once the sandbox has established the SDK) + * and hands them to the CI Visibility core via {@link AndroidTestContext}. + */ +public final class RobolectricTestExtractor { + + /** Matches the version in a {@code robolectric-.jar} file name. */ + private static final Pattern ROBOLECTRIC_JAR = Pattern.compile("^robolectric-(.+)\\.jar$"); + + private RobolectricTestExtractor() {} + + public static void capture() { + int apiLevel = RuntimeEnvironment.getApiLevel(); + if (apiLevel <= 0) { + return; + } + String release = null; + String codename = null; + AndroidVersions.AndroidRelease androidRelease = AndroidVersions.getReleaseForSdkInt(apiLevel); + if (androidRelease != null) { + release = androidRelease.getVersion(); + codename = androidRelease.getShortCode(); + } + AndroidTestContext.set(new AndroidTestInfo(apiLevel, release, codename, 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..635066e21a8 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 requires JDK 17+ for its sandbox. + Assumptions.assumeTrue( + JavaVirtualMachine.isJavaVersionAtLeast(17), "Robolectric requires JDK 17 or higher"); + + 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..a40de6565b2 --- /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" : 21, + "test.source.start" : 13 + }, + "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" : 20, + "test.source.start" : 17 + }, + "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/android/AndroidTestContext.java b/internal-api/src/main/java/datadog/trace/api/civisibility/android/AndroidTestContext.java new file mode 100644 index 00000000000..41d25f1b4c4 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/android/AndroidTestContext.java @@ -0,0 +1,42 @@ +package datadog.trace.api.civisibility.android; + +import javax.annotation.Nullable; + +/** + * Thread-local hand-off for {@link AndroidTestInfo} between the Robolectric instrumentation and the + * CI Visibility core. + * + *

Robolectric runs each test on a per-SDK sandbox "main" thread, and the emulated SDK is only + * established while the test body executes. The Robolectric instrumentation captures the SDK on + * that sandbox thread (via {@link #set}) while it is available, and the core drains it (via {@link + * #getAndClear}) on the same thread when the test finishes. + */ +public abstract class AndroidTestContext { + + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + private AndroidTestContext() {} + + /** Records the emulated Android environment for the test executing on the current thread. */ + public static void set(AndroidTestInfo info) { + CURRENT.set(info); + } + + /** + * Returns and clears the info recorded for the current thread, or {@code null} if none (the + * common, non-Robolectric case). + */ + @Nullable + public static AndroidTestInfo getAndClear() { + AndroidTestInfo info = CURRENT.get(); + if (info != null) { + CURRENT.remove(); + } + return info; + } + + /** Clears any info recorded for the current thread. */ + public static void clear() { + CURRENT.remove(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/android/AndroidTestInfo.java b/internal-api/src/main/java/datadog/trace/api/civisibility/android/AndroidTestInfo.java new file mode 100644 index 00000000000..e834b6711c5 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/android/AndroidTestInfo.java @@ -0,0 +1,46 @@ +package datadog.trace.api.civisibility.android; + +import javax.annotation.Nullable; + +/** Immutable snapshot of the emulated Android SDK a test ran against (e.g. under Robolectric). */ +public final class AndroidTestInfo { + + private final int apiLevel; + @Nullable private final String release; + @Nullable private final String codename; + @Nullable private final String robolectricVersion; + + public AndroidTestInfo( + int apiLevel, + @Nullable String release, + @Nullable String codename, + @Nullable String robolectricVersion) { + this.apiLevel = apiLevel; + this.release = release; + this.codename = codename; + this.robolectricVersion = robolectricVersion; + } + + /** The emulated Android API level (e.g. {@code 34}). */ + public int getApiLevel() { + return apiLevel; + } + + /** The Android release the API level maps to (e.g. {@code "14"}), or {@code null} if unknown. */ + @Nullable + public String getRelease() { + return release; + } + + /** The Android release short code (e.g. {@code "U"}), or {@code null} if unknown. */ + @Nullable + public String getCodename() { + return codename; + } + + /** The Robolectric version driving the test (e.g. {@code "4.16.1"}), or {@code null}. */ + @Nullable + public String getRobolectricVersion() { + return robolectricVersion; + } +} 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..e4d5d38c8d2 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.IsAndroid; 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, + IsAndroid.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/IsAndroid.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsAndroid.java new file mode 100644 index 00000000000..e39baaa8d52 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsAndroid.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 IsAndroid implements TagValue { + TRUE; + + @Override + public String asString() { + return "is_android: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/internal-api/src/test/java/datadog/trace/api/civisibility/android/AndroidTestContextTest.java b/internal-api/src/test/java/datadog/trace/api/civisibility/android/AndroidTestContextTest.java new file mode 100644 index 00000000000..41d0492cf7c --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/civisibility/android/AndroidTestContextTest.java @@ -0,0 +1,45 @@ +package datadog.trace.api.civisibility.android; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class AndroidTestContextTest { + + @AfterEach + void reset() { + AndroidTestContext.clear(); + } + + @Test + void getAndClearReturnsTheStoredInfoOnce() { + AndroidTestInfo info = new AndroidTestInfo(34, "14", "U", "4.16.1"); + AndroidTestContext.set(info); + + AndroidTestInfo drained = AndroidTestContext.getAndClear(); + assertNotNull(drained); + assertEquals(info, drained); + assertEquals(34, drained.getApiLevel()); + assertEquals("14", drained.getRelease()); + assertEquals("U", drained.getCodename()); + assertEquals("4.16.1", drained.getRobolectricVersion()); + + // A second drain returns nothing: the value is consumed. + assertNull(AndroidTestContext.getAndClear()); + } + + @Test + void getAndClearReturnsNullWhenNothingRecorded() { + assertNull(AndroidTestContext.getAndClear()); + } + + @Test + void clearRemovesTheStoredInfo() { + AndroidTestContext.set(new AndroidTestInfo(30, null, null, null)); + AndroidTestContext.clear(); + assertNull(AndroidTestContext.getAndClear()); + } +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 60dae10c020..019d5605ae8 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -9537,6 +9537,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 53fe2bff423..73f2297330e 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",