diff --git a/testing-common/integration-tests/build.gradle.kts b/testing-common/integration-tests/build.gradle.kts index 49acf4af7b..067d71082b 100644 --- a/testing-common/integration-tests/build.gradle.kts +++ b/testing-common/integration-tests/build.gradle.kts @@ -11,6 +11,8 @@ dependencies { testCompileOnly(project(":javaagent-bootstrap")) testCompileOnly(project(":javaagent-extension-api")) testCompileOnly(project(":muzzle")) + testCompileOnly("com.google.auto.service:auto-service-annotations") + testCompileOnly("com.google.code.findbugs:annotations") testImplementation("net.bytebuddy:byte-buddy") testImplementation("net.bytebuddy:byte-buddy-agent") @@ -47,25 +49,10 @@ tasks { jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } - val testIndyModuleOldBytecodeInstrumentation by registering(Test::class) { - filter { - includeTestsMatching("InstrumentOldBytecode") - } - include("**/InstrumentOldBytecode.*") - } - - val testInlineModuleOldBytecodeInstrumentation by registering(Test::class) { - filter { - includeTestsMatching("InstrumentOldBytecode") - } - include("**/InstrumentOldBytecode.*") - } - test { filter { excludeTestsMatching("context.FieldInjectionDisabledTest") excludeTestsMatching("context.FieldBackedImplementationTest") - excludeTestsMatching("InstrumentOldBytecode") } // this is needed for AgentInstrumentationSpecificationTest jvmArgs("-Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass\$NestedClass") diff --git a/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy b/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy deleted file mode 100644 index e6af2be22d..0000000000 --- a/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.google.common.reflect.ClassPath -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.test.utils.ClasspathUtils -import io.opentelemetry.javaagent.bootstrap.BootstrapPackagePrefixesHolder -import org.slf4j.LoggerFactory - -import java.util.concurrent.TimeoutException - -// this test is run using -// -Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass$NestedClass -// (see integration-tests.gradle) -class AgentInstrumentationSpecificationTest extends AgentInstrumentationSpecification { - private static final ClassLoader BOOTSTRAP_CLASSLOADER = null - - public static final List BOOTSTRAP_PACKAGE_PREFIXES = BootstrapPackagePrefixesHolder.getBoostrapPackagePrefixes() - - def "classpath setup"() { - setup: - final List bootstrapClassesIncorrectlyLoaded = [] - for (ClassPath.ClassInfo info : getTestClasspath().getAllClasses()) { - for (int i = 0; i < BOOTSTRAP_PACKAGE_PREFIXES.size(); ++i) { - if (info.getName().startsWith(BOOTSTRAP_PACKAGE_PREFIXES[i])) { - Class bootstrapClass = Class.forName(info.getName()) - def loader - try { - loader = bootstrapClass.getClassLoader() - } catch (NoClassDefFoundError e) { - // some classes in com.google.errorprone.annotations cause groovy to throw - // java.lang.NoClassDefFoundError: [Ljavax/lang/model/element/Modifier; - break - } - if (loader != BOOTSTRAP_CLASSLOADER) { - bootstrapClassesIncorrectlyLoaded.add(bootstrapClass) - } - break - } - } - } - - expect: - bootstrapClassesIncorrectlyLoaded == [] - } - - def "waiting for child spans times out"() { - when: - runWithSpan("parent") { - waitForTraces(1) - } - - then: - thrown(TimeoutException) - } - - def "logging works"() { - when: - LoggerFactory.getLogger(AgentInstrumentationSpecificationTest).debug("hello") - then: - noExceptionThrown() - } - - def "excluded classes are not instrumented"() { - when: - runWithSpan("parent") { - subject.getConstructor().newInstance().run() - } - - then: - assertTraces(1) { - trace(0, spanName ? 2 : 1) { - span(0) { - name "parent" - } - if (spanName) { - span(1) { - name spanName - childOf span(0) - } - } - } - } - - where: - subject | spanName - config.SomeClass | "SomeClass.run" - config.SomeClass.NestedClass | "NestedClass.run" - config.exclude.SomeClass | null - config.exclude.SomeClass.NestedClass | null - config.exclude.packagename.SomeClass | null - config.exclude.packagename.SomeClass.NestedClass | null - } - - def "test unblocked by completed span"() { - setup: - runWithSpan("parent") { - runWithSpan("child") {} - } - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - hasNoParent() - } - span(1) { - name "child" - childOf span(0) - } - } - } - } - - private static ClassPath getTestClasspath() { - ClassLoader testClassLoader = ClasspathUtils.getClassLoader() - if (!(testClassLoader instanceof URLClassLoader)) { - // java9's system loader does not extend URLClassLoader - // which breaks Guava ClassPath lookup - testClassLoader = buildJavaClassPathClassLoader() - } - try { - return ClassPath.from(testClassLoader) - } catch (IOException e) { - throw new IllegalStateException(e) - } - } - - /** - * Parse JVM classpath and return ClassLoader containing all classpath entries. Inspired by Guava. - */ - private static ClassLoader buildJavaClassPathClassLoader() { - List urls = new ArrayList<>() - for (String entry : getClasspath()) { - try { - try { - urls.add(new File(entry).toURI().toURL()) - } catch (SecurityException e) { // File.toURI checks to see if the file is a directory - urls.add(new URL("file", null, new File(entry).getAbsolutePath())) - } - } catch (MalformedURLException e) { - throw new IllegalStateException(e) - } - } - return new URLClassLoader(urls.toArray(new URL[0]), (ClassLoader) null) - } - - private static String[] getClasspath() { - return System.getProperty("java.class.path").split(System.getProperty("path.separator")) - } -} diff --git a/testing-common/integration-tests/src/test/groovy/InstrumentOldBytecode.groovy b/testing-common/integration-tests/src/test/groovy/InstrumentOldBytecode.groovy deleted file mode 100644 index 629be499d9..0000000000 --- a/testing-common/integration-tests/src/test/groovy/InstrumentOldBytecode.groovy +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.ibm.as400.resource.ResourceLevel -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification - -class InstrumentOldBytecode extends AgentInstrumentationSpecification { - def "can instrument old bytecode"() { - expect: - new ResourceLevel().toString() == "instrumented" - } -} diff --git a/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy b/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy deleted file mode 100644 index b74ed226b4..0000000000 --- a/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package context - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.test.utils.ClasspathUtils -import io.opentelemetry.instrumentation.test.utils.GcUtils -import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess -import library.KeyClass -import library.UntransformableKeyClass -import net.bytebuddy.agent.ByteBuddyAgent -import net.sf.cglib.proxy.Enhancer -import net.sf.cglib.proxy.MethodInterceptor -import net.sf.cglib.proxy.MethodProxy -import spock.lang.Unroll - -import java.lang.instrument.ClassDefinition -import java.lang.ref.WeakReference -import java.lang.reflect.Field -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.time.Duration -import java.util.concurrent.atomic.AtomicReference - -// this test is run using -// -Dotel.instrumentation.context-test-instrumentation.enabled=true -// (see integration-tests.gradle) -class FieldBackedImplementationTest extends AgentInstrumentationSpecification { - - def setupSpec() { - TestAgentListenerAccess.addSkipErrorCondition({ typeName, throwable -> - return typeName.startsWith('library.Incorrect') && - throwable.getMessage().startsWith("Incorrect Context Api Usage detected.") - }) - TestAgentListenerAccess.addSkipTransformationCondition({ typeName -> - return typeName != null && typeName.endsWith("UntransformableKeyClass") - }) - } - - @Unroll - def "#keyClassName structure modified = #shouldModifyStructure"() { - setup: - boolean hasField = false - boolean isPrivate = false - boolean isTransient = false - boolean isSynthetic = false - for (Field field : keyClass.getDeclaredFields()) { - if (field.getName().startsWith("__opentelemetry")) { - isPrivate = Modifier.isPrivate(field.getModifiers()) - isTransient = Modifier.isTransient(field.getModifiers()) - isSynthetic = field.isSynthetic() - hasField = true - break - } - } - - boolean hasMarkerInterface = false - boolean hasAccessorInterface = false - boolean accessorInterfaceIsSynthetic = false - for (Class inter : keyClass.getInterfaces()) { - if (inter.getName() == 'io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker') { - hasMarkerInterface = true - } - if (inter.getName().startsWith('io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessor$')) { - hasAccessorInterface = true - accessorInterfaceIsSynthetic = inter.isSynthetic() - } - } - - expect: - hasField == shouldModifyStructure - isPrivate == shouldModifyStructure - isTransient == shouldModifyStructure - isSynthetic == shouldModifyStructure - hasMarkerInterface == shouldModifyStructure - hasAccessorInterface == shouldModifyStructure - accessorInterfaceIsSynthetic == shouldModifyStructure - keyClass.newInstance().isInstrumented() == shouldModifyStructure - - where: - keyClass | keyClassName | shouldModifyStructure - KeyClass | keyClass.getSimpleName() | true - UntransformableKeyClass | keyClass.getSimpleName() | false - } - - def "multiple fields are injected"() { - setup: - List fields = [] - for (Field field : KeyClass.getDeclaredFields()) { - if (field.getName().startsWith("__opentelemetry")) { - fields.add(field) - } - } - - List> interfaces = [] - for (Class iface : KeyClass.getInterfaces()) { - if (iface.name.startsWith('io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessor$')) { - interfaces.add(iface) - } - } - - expect: - fields.size() == 3 - fields.forEach { field -> - assert Modifier.isPrivate(field.modifiers) - assert Modifier.isTransient(field.modifiers) - assert field.synthetic - } - - interfaces.size() == 3 - interfaces.forEach { iface -> - assert iface.synthetic - } - } - - def "correct api usage stores state in map"() { - when: - instance1.incrementContextCount() - - then: - instance1.incrementContextCount() == 2 - instance2.incrementContextCount() == 1 - - where: - instance1 | instance2 - new KeyClass() | new KeyClass() - new UntransformableKeyClass() | new UntransformableKeyClass() - } - - def "get/put test"() { - when: - instance1.putContextCount(10) - - then: - instance1.getContextCount() == 10 - - where: - instance1 | _ - new KeyClass() | _ - new UntransformableKeyClass() | _ - } - - def "remove test"() { - given: - instance1.putContextCount(10) - - when: - instance1.removeContextCount() - - then: - instance1.getContextCount() == 0 - - where: - instance1 | _ - new KeyClass() | _ - new UntransformableKeyClass() | _ - } - - def "works with cglib enhanced instances which duplicates context getter and setter methods"() { - setup: - Enhancer enhancer = new Enhancer() - enhancer.setSuperclass(KeyClass) - enhancer.setCallback(new MethodInterceptor() { - @Override - Object intercept(Object arg0, Method arg1, Object[] arg2, - MethodProxy arg3) throws Throwable { - return arg3.invokeSuper(arg0, arg2) - } - }) - - when: - (KeyClass) enhancer.create() - - then: - noExceptionThrown() - } - - def "backing map should not create strong refs to key class instances #keyValue.get().getClass().getName()"() { - when: - int count = keyValue.get().incrementContextCount() - WeakReference instanceRef = new WeakReference(keyValue.get()) - keyValue.set(null) - GcUtils.awaitGc(instanceRef, Duration.ofSeconds(10)) - - then: - instanceRef.get() == null - count == 1 - - where: - keyValue | _ - new AtomicReference(new KeyClass()) | _ - new AtomicReference(new UntransformableKeyClass()) | _ - } - - def "context classes are retransform safe"() { - when: - ByteBuddyAgent.install() - ByteBuddyAgent.getInstrumentation().retransformClasses(KeyClass) - ByteBuddyAgent.getInstrumentation().retransformClasses(UntransformableKeyClass) - - then: - new KeyClass().isInstrumented() - !new UntransformableKeyClass().isInstrumented() - new KeyClass().incrementContextCount() == 1 - new UntransformableKeyClass().incrementContextCount() == 1 - } - - // NB: This test will fail if some other agent is also running that modifies the class structure - // in a way that is incompatible with redefining the class back to its original bytecode. - // A likely culprit is jacoco if you start seeing failure here due to a change make sure jacoco - // exclusion is working. - def "context classes are redefine safe"() { - when: - ByteBuddyAgent.install() - ByteBuddyAgent.getInstrumentation().redefineClasses(new ClassDefinition(KeyClass, ClasspathUtils.convertToByteArray(KeyClass))) - ByteBuddyAgent.getInstrumentation().redefineClasses(new ClassDefinition(UntransformableKeyClass, ClasspathUtils.convertToByteArray(UntransformableKeyClass))) - - then: - new KeyClass().isInstrumented() - !new UntransformableKeyClass().isInstrumented() - new KeyClass().incrementContextCount() == 1 - new UntransformableKeyClass().incrementContextCount() == 1 - } -} diff --git a/testing-common/integration-tests/src/test/groovy/context/FieldInjectionDisabledTest.groovy b/testing-common/integration-tests/src/test/groovy/context/FieldInjectionDisabledTest.groovy deleted file mode 100644 index c77c8b6a04..0000000000 --- a/testing-common/integration-tests/src/test/groovy/context/FieldInjectionDisabledTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package context - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess -import library.DisabledKeyClass - -import java.lang.reflect.Field - -// this test is run using: -// -Dotel.javaagent.experimental.field-injection.enabled=false -// -Dotel.instrumentation.context-test-instrumentation.enabled=true -// (see integration-tests.gradle) -class FieldInjectionDisabledTest extends AgentInstrumentationSpecification { - - def setupSpec() { - TestAgentListenerAccess.addSkipErrorCondition({ typeName, throwable -> - return typeName.startsWith(ContextTestInstrumentationModule.getName() + '$Incorrect') && throwable.getMessage().startsWith("Incorrect Context Api Usage detected.") - }) - } - - def "Check that structure is not modified when structure modification is disabled"() { - setup: - def keyClass = DisabledKeyClass - boolean hasField = false - for (Field field : keyClass.getDeclaredFields()) { - if (field.getName().startsWith("__opentelemetry")) { - hasField = true - break - } - } - - boolean hasMarkerInterface = false - boolean hasAccessorInterface = false - for (Class inter : keyClass.getInterfaces()) { - if (inter.getName() == 'io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker') { - hasMarkerInterface = true - } - if (inter.getName().startsWith('io.opentelemetry.javaagent.bootstrap.instrumentation.context.FieldBackedProvider$ContextAccessor')) { - hasAccessorInterface = true - } - } - - expect: - hasField == false - hasMarkerInterface == false - hasAccessorInterface == false - keyClass.newInstance().isInstrumented() == true - } -} diff --git a/testing-common/integration-tests/src/test/java/context/FieldBackedImplementationTest.java b/testing-common/integration-tests/src/test/java/context/FieldBackedImplementationTest.java new file mode 100644 index 0000000000..1cd4ce7e88 --- /dev/null +++ b/testing-common/integration-tests/src/test/java/context/FieldBackedImplementationTest.java @@ -0,0 +1,237 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.test.utils.ClasspathUtils; +import io.opentelemetry.instrumentation.test.utils.GcUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess; +import java.lang.instrument.ClassDefinition; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import library.KeyClass; +import library.UntransformableKeyClass; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +// this test is run using +// -Dotel.instrumentation.context-test-instrumentation.enabled=true +// (see integration-tests.gradle) +class FieldBackedImplementationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeAll + static void setUp() { + TestAgentListenerAccess.addSkipErrorCondition( + (typeName, throwable) -> + typeName.startsWith("library.Incorrect") + && throwable.getMessage().startsWith("Incorrect Context Api Usage detected.")); + TestAgentListenerAccess.addSkipTransformationCondition( + typeName -> typeName != null && typeName.endsWith("UntransformableKeyClass")); + } + + private static Stream provideKeyClassParameters() { + return Stream.of(Arguments.of(KeyClass.class), Arguments.of(UntransformableKeyClass.class)); + } + + @ParameterizedTest + @MethodSource("provideKeyClassParameters") + void structureModified(Class keyClass) throws Exception { + boolean shouldModifyStructure = !keyClass.equals(UntransformableKeyClass.class); + boolean hasField = false; + boolean isPrivate = false; + boolean isTransient = false; + boolean isSynthetic = false; + for (Field field : keyClass.getDeclaredFields()) { + if (field.getName().startsWith("__opentelemetry")) { + isPrivate = Modifier.isPrivate(field.getModifiers()); + isTransient = Modifier.isTransient(field.getModifiers()); + isSynthetic = field.isSynthetic(); + hasField = true; + break; + } + } + + boolean hasMarkerInterface = false; + boolean hasAccessorInterface = false; + boolean accessorInterfaceIsSynthetic = false; + for (Class iface : keyClass.getInterfaces()) { + if ("io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker" + .equals(iface.getName())) { + hasMarkerInterface = true; + } + if (iface + .getName() + .startsWith("io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessor$")) { + hasAccessorInterface = true; + accessorInterfaceIsSynthetic = iface.isSynthetic(); + } + } + + assertThat(hasField).isEqualTo(shouldModifyStructure); + assertThat(isPrivate).isEqualTo(shouldModifyStructure); + assertThat(isTransient).isEqualTo(shouldModifyStructure); + assertThat(isSynthetic).isEqualTo(shouldModifyStructure); + assertThat(hasMarkerInterface).isEqualTo(shouldModifyStructure); + assertThat(hasAccessorInterface).isEqualTo(shouldModifyStructure); + assertThat(accessorInterfaceIsSynthetic).isEqualTo(shouldModifyStructure); + assertThat(keyClass.getConstructor().newInstance().isInstrumented()) + .isEqualTo(shouldModifyStructure); + } + + @Test + void multipleFieldsInjected() { + List fields = new ArrayList<>(); + for (Field field : KeyClass.class.getDeclaredFields()) { + if (field.getName().startsWith("__opentelemetry")) { + fields.add(field); + } + } + + List> interfaces = new ArrayList<>(); + for (Class iface : KeyClass.class.getInterfaces()) { + if (iface + .getName() + .startsWith("io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessor$")) { + interfaces.add(iface); + } + } + + assertThat(fields.size()).isEqualTo(3); + assertThat(fields) + .allSatisfy( + field -> { + assertThat(Modifier.isPrivate(field.getModifiers())).isTrue(); + assertThat(Modifier.isTransient(field.getModifiers())).isTrue(); + assertThat(field.isSynthetic()).isTrue(); + }); + + assertThat(interfaces.size()).isEqualTo(3); + assertThat(interfaces).allSatisfy(iface -> assertThat(iface.isSynthetic()).isTrue()); + } + + @ParameterizedTest + @MethodSource("provideKeyClassParameters") + void instanceState(Class keyClass) throws Exception { + KeyClass instance1 = keyClass.getConstructor().newInstance(); + KeyClass instance2 = keyClass.getConstructor().newInstance(); + + // correct api usage stores state in map + instance1.incrementContextCount(); + + assertThat(instance1.incrementContextCount()).isEqualTo(2); + assertThat(instance2.incrementContextCount()).isEqualTo(1); + } + + @ParameterizedTest + @MethodSource("provideKeyClassParameters") + void modifyInstanceState(Class keyClass) throws Exception { + KeyClass instance1 = keyClass.getConstructor().newInstance(); + instance1.putContextCount(10); + + assertThat(instance1.getContextCount()).isEqualTo(10); + } + + @ParameterizedTest + @MethodSource("provideKeyClassParameters") + void removeInstanceState(Class keyClass) throws Exception { + KeyClass instance1 = keyClass.getConstructor().newInstance(); + instance1.putContextCount(10); + instance1.removeContextCount(); + + assertThat(instance1.getContextCount()).isEqualTo(0); + } + + @Test + void cglibProxy() { + // works with cglib enhanced instances which duplicates context getter and setter methods + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(KeyClass.class); + enhancer.setCallback( + new MethodInterceptor() { + @Override + public Object intercept( + Object instance, Method method, Object[] arguments, MethodProxy methodProxy) + throws Throwable { + return methodProxy.invokeSuper(instance, arguments); + } + }); + + assertThat(enhancer.create()).isInstanceOf(KeyClass.class); + } + + @ParameterizedTest + @MethodSource("provideKeyClassParameters") + @SuppressWarnings("UnnecessaryAsync") + void instanceStateGc(Class keyClass) throws Exception { + // backing map should not create strong refs to key class instances + AtomicReference keyValue = + new AtomicReference<>(keyClass.getConstructor().newInstance()); + int count = keyValue.get().incrementContextCount(); + WeakReference instanceRef = new WeakReference<>(keyValue.get()); + keyValue.set(null); + GcUtils.awaitGc(instanceRef, Duration.ofSeconds(10)); + + assertThat(instanceRef.get()).isNull(); + assertThat(count).isEqualTo(1); + } + + @Test + void retransform() throws Exception { + // context classes are retransform safe + ByteBuddyAgent.install(); + ByteBuddyAgent.getInstrumentation().retransformClasses(KeyClass.class); + ByteBuddyAgent.getInstrumentation().retransformClasses(UntransformableKeyClass.class); + + assertThat(new KeyClass().isInstrumented()).isTrue(); + assertThat(new UntransformableKeyClass().isInstrumented()).isFalse(); + assertThat(new KeyClass().incrementContextCount()).isEqualTo(1); + assertThat(new UntransformableKeyClass().incrementContextCount()).isEqualTo(1); + } + + // NB: This test will fail if some other agent is also running that modifies the class structure + // in a way that is incompatible with redefining the class back to its original bytecode. + // A likely culprit is jacoco if you start seeing failure here due to a change make sure jacoco + // exclusion is working. + @Test + void redefine() throws Exception { + // context classes are redefine safe + ByteBuddyAgent.install(); + ByteBuddyAgent.getInstrumentation() + .redefineClasses( + new ClassDefinition(KeyClass.class, ClasspathUtils.convertToByteArray(KeyClass.class))); + ByteBuddyAgent.getInstrumentation() + .redefineClasses( + new ClassDefinition( + UntransformableKeyClass.class, + ClasspathUtils.convertToByteArray(UntransformableKeyClass.class))); + + assertThat(new KeyClass().isInstrumented()).isTrue(); + assertThat(new UntransformableKeyClass().isInstrumented()).isFalse(); + assertThat(new KeyClass().incrementContextCount()).isEqualTo(1); + assertThat(new UntransformableKeyClass().incrementContextCount()).isEqualTo(1); + } +} diff --git a/testing-common/integration-tests/src/test/java/context/FieldInjectionDisabledTest.java b/testing-common/integration-tests/src/test/java/context/FieldInjectionDisabledTest.java new file mode 100644 index 0000000000..5b6db62b6e --- /dev/null +++ b/testing-common/integration-tests/src/test/java/context/FieldInjectionDisabledTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess; +import java.lang.reflect.Field; +import library.DisabledKeyClass; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +// this test is run using: +// -Dotel.javaagent.experimental.field-injection.enabled=false +// -Dotel.instrumentation.context-test-instrumentation.enabled=true +// (see integration-tests.gradle) +class FieldInjectionDisabledTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeAll + static void setUp() { + TestAgentListenerAccess.addSkipErrorCondition( + (typeName, throwable) -> + typeName.startsWith(ContextTestInstrumentationModule.class.getName() + "$Incorrect") + && throwable.getMessage().startsWith("Incorrect Context Api Usage detected.")); + } + + @Test + void structuralModificationDisabled() { + Class keyClass = DisabledKeyClass.class; + boolean hasField = false; + for (Field field : keyClass.getDeclaredFields()) { + if (field.getName().startsWith("__opentelemetry")) { + hasField = true; + break; + } + } + + boolean hasMarkerInterface = false; + boolean hasAccessorInterface = false; + for (Class inter : keyClass.getInterfaces()) { + if ("io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker" + .equals(inter.getName())) { + hasMarkerInterface = true; + } + if (inter + .getName() + .startsWith( + "io.opentelemetry.javaagent.bootstrap.instrumentation.context.FieldBackedProvider$ContextAccessor")) { + hasAccessorInterface = true; + } + } + + assertThat(hasField).isFalse(); + assertThat(hasMarkerInterface).isFalse(); + assertThat(hasAccessorInterface).isFalse(); + assertThat(new DisabledKeyClass().isInstrumented()).isTrue(); + } +} diff --git a/testing-common/integration-tests/src/test/java/instrumentation/AgentInstrumentationTest.java b/testing-common/integration-tests/src/test/java/instrumentation/AgentInstrumentationTest.java new file mode 100644 index 0000000000..c83a2161a5 --- /dev/null +++ b/testing-common/integration-tests/src/test/java/instrumentation/AgentInstrumentationTest.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package instrumentation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.reflect.ClassPath; +import io.opentelemetry.instrumentation.test.utils.ClasspathUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.bootstrap.BootstrapPackagePrefixesHolder; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.LoggerFactory; + +// this test is run using +// +// -Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass$NestedClass +// (see integration-tests.gradle) +class AgentInstrumentationTest { + + private static final ClassLoader BOOTSTRAP_CLASSLOADER = null; + private static final List BOOTSTRAP_PACKAGE_PREFIXES = + BootstrapPackagePrefixesHolder.getBoostrapPackagePrefixes(); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void classPathSetUp() throws ClassNotFoundException { + List> bootstrapClassesIncorrectlyLoaded = new ArrayList<>(); + for (ClassPath.ClassInfo info : getTestClasspath().getAllClasses()) { + for (String bootstrapPrefix : BOOTSTRAP_PACKAGE_PREFIXES) { + if (info.getName().startsWith(bootstrapPrefix)) { + Class bootstrapClass = Class.forName(info.getName()); + ClassLoader loader = bootstrapClass.getClassLoader(); + if (loader != BOOTSTRAP_CLASSLOADER) { + bootstrapClassesIncorrectlyLoaded.add(bootstrapClass); + } + } + } + } + + assertThat(bootstrapClassesIncorrectlyLoaded).isEmpty(); + } + + @Test + void waitingForChildSpansTimesOut() { + assertThatThrownBy(() -> testing.runWithSpan("parent", () -> testing.waitForTraces(1))) + .isInstanceOf(AssertionError.class) + .hasMessage("Error waiting for 1 traces"); + } + + @Test + void loggingWorks() { + assertThatNoException() + .isThrownBy(() -> LoggerFactory.getLogger(AgentInstrumentationTest.class).debug("hello")); + } + + private static Stream provideExcludedClassTestParameters() { + return Stream.of( + Arguments.of(config.SomeClass.class, "SomeClass.run"), + Arguments.of(config.SomeClass.NestedClass.class, "NestedClass.run"), + Arguments.of(config.exclude.SomeClass.class, null), + Arguments.of(config.exclude.SomeClass.NestedClass.class, null), + Arguments.of(config.exclude.packagename.SomeClass.class, null), + Arguments.of(config.exclude.packagename.SomeClass.NestedClass.class, null)); + } + + @ParameterizedTest + @MethodSource("provideExcludedClassTestParameters") + void excludedClassesAreNotInstrumented(Class subject, String spanName) + throws Exception { + testing.runWithSpan("parent", () -> subject.getConstructor().newInstance().run()); + + testing.waitAndAssertTraces( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add(span -> span.hasName("parent").hasNoParent()); + if (spanName != null) { + assertions.add(span -> span.hasName(spanName).hasParent(trace.getSpan(0))); + } + trace.hasSpansSatisfyingExactly(assertions); + }); + } + + @Test + void testUnblockedByCompletedSpan() { + testing.runWithSpan("parent", () -> testing.runWithSpan("child", () -> {})); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> span.hasName("child").hasParent(trace.getSpan(0)))); + } + + private static ClassPath getTestClasspath() { + ClassLoader testClassLoader = ClasspathUtils.class.getClassLoader(); + if (!(testClassLoader instanceof URLClassLoader)) { + // java9's system loader does not extend URLClassLoader + // which breaks Guava ClassPath lookup + testClassLoader = buildJavaClassPathClassLoader(); + } + try { + return ClassPath.from(testClassLoader); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * Parse JVM classpath and return ClassLoader containing all classpath entries. Inspired by Guava. + */ + private static ClassLoader buildJavaClassPathClassLoader() { + List urls = new ArrayList<>(); + for (String entry : getClasspath()) { + try { + try { + urls.add(new File(entry).toURI().toURL()); + } catch (SecurityException e) { // File.toURI checks to see if the file is a directory + urls.add(new URL("file", null, new File(entry).getAbsolutePath())); + } + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + return new URLClassLoader(urls.toArray(new URL[0]), null); + } + + private static String[] getClasspath() { + return System.getProperty("java.class.path").split(System.getProperty("path.separator")); + } +} diff --git a/testing-common/integration-tests/src/test/java/instrumentation/InstrumentOldBytecodeTest.java b/testing-common/integration-tests/src/test/java/instrumentation/InstrumentOldBytecodeTest.java new file mode 100644 index 0000000000..552e5ec63d --- /dev/null +++ b/testing-common/integration-tests/src/test/java/instrumentation/InstrumentOldBytecodeTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package instrumentation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class InstrumentOldBytecodeTest { + + @Test + @SuppressWarnings("deprecation") // com.ibm.as400.resource.ResourceLevel is deprecated + void canInstrumentOldBytecode() { + assertThat(new com.ibm.as400.resource.ResourceLevel().toString()).isEqualTo("instrumented"); + } +}