diff --git a/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts b/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts index 7d3c8d1d53..48c88f5279 100644 --- a/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts @@ -105,8 +105,9 @@ tasks { // some moving. disable("DefaultPackage") - // we use modified OtelPrivateConstructorForUtilityClass which ignores *Advice classes + // we use modified Otel* checks which ignore *Advice classes disable("PrivateConstructorForUtilityClass") + disable("CanIgnoreReturnValueSuggester") // TODO(anuraaga): Remove this, probably after instrumenter API migration instead of dealing // with older APIs. @@ -125,9 +126,9 @@ tasks { // Allow underscore in test-type method names disable("MemberName") } - if (project.path.endsWith(":testing") || name.contains("Test")) { + if ((project.path.endsWith(":testing") || name.contains("Test")) && !project.name.equals("custom-checks")) { // This check causes too many failures, ignore the ones in tests - disable("CanIgnoreReturnValueSuggester") + disable("OtelCanIgnoreReturnValueSuggester") } } } diff --git a/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java b/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java new file mode 100644 index 0000000000..3c97624577 --- /dev/null +++ b/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.customchecks; + +import static com.google.errorprone.matchers.Description.NO_MATCH; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.TreePath; + +@AutoService(BugChecker.class) +@BugPattern( + summary = + "Methods with ignorable return values (including methods that always 'return this') should be annotated with @com.google.errorprone.annotations.CanIgnoreReturnValue", + severity = BugPattern.SeverityLevel.WARNING) +public class OtelCanIgnoreReturnValueSuggester extends BugChecker + implements BugChecker.MethodTreeMatcher { + + private static final long serialVersionUID = 1L; + + private final CanIgnoreReturnValueSuggester delegate = new CanIgnoreReturnValueSuggester(); + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState visitorState) { + ClassTree containerClass = findContainingClass(visitorState.getPath()); + if (containerClass.getSimpleName().toString().endsWith("Advice")) { + return NO_MATCH; + } + Description description = delegate.matchMethod(methodTree, visitorState); + if (description == NO_MATCH) { + return description; + } + return describeMatch(methodTree); + } + + private static ClassTree findContainingClass(TreePath path) { + TreePath parent = path.getParentPath(); + while (parent != null && !(parent.getLeaf() instanceof ClassTree)) { + parent = parent.getParentPath(); + } + if (parent == null) { + throw new IllegalStateException( + "Method is expected to be contained in a class, something must be wrong"); + } + ClassTree containerClass = (ClassTree) parent.getLeaf(); + return containerClass; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java index be34dcb8d1..146dcf7f83 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java @@ -82,7 +82,7 @@ public final class SupportabilityMetrics { } // this private method is designed for assignment of the return value - @SuppressWarnings("CanIgnoreReturnValueSuggester") + @SuppressWarnings("OtelCanIgnoreReturnValueSuggester") private SupportabilityMetrics start() { if (agentDebugEnabled) { ScheduledExecutorService executor = diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java index 4d2d55862c..d7ca2dc941 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java @@ -145,7 +145,7 @@ public class WeakConcurrentMap private K key; private int hashCode; - @SuppressWarnings("CanIgnoreReturnValueSuggester") + @SuppressWarnings("OtelCanIgnoreReturnValueSuggester") LookupKey withValue(K key) { this.key = key; hashCode = System.identityHashCode(key); diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java index 40223d460c..ac2c0ffbd5 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java @@ -7,7 +7,7 @@ package io.opentelemetry.instrumentation.micrometer.v1_5; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; -@SuppressWarnings("CanIgnoreReturnValueSuggester") +@SuppressWarnings("OtelCanIgnoreReturnValueSuggester") enum DistributionStatisticConfigModifier { DISABLE_HISTOGRAM_GAUGES { @Override diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java new file mode 100644 index 0000000000..efc6e4730e --- /dev/null +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; + +/** + * Contains the bootstrap method for initializing invokedynamic callsites which are added via agent + * instrumentation. + */ +public class IndyBootstrapDispatcher { + + private static volatile MethodHandle bootstrap; + + private IndyBootstrapDispatcher() {} + + /** + * Initialized the invokedynamic bootstrapping method to which this class will delegate. + * + * @param bootstrapMethod the method to delegate to. Must have the same type as {@link + * #bootstrap}. + */ + public static void init(MethodHandle bootstrapMethod) { + bootstrap = bootstrapMethod; + } + + public static CallSite bootstrap( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType adviceMethodType, + Object... args) { + CallSite callSite = null; + if (bootstrap != null) { + try { + callSite = (CallSite) bootstrap.invoke(lookup, adviceMethodName, adviceMethodType, args); + } catch (Throwable e) { + ExceptionLogger.logSuppressedError("Error bootstrapping indy instruction", e); + } + } + if (callSite == null) { + // The MethodHandle pointing to the Advice could not be created for some reason, + // fallback to a Noop MethodHandle to not crash the application + MethodHandle noop = generateNoopMethodHandle(adviceMethodType); + callSite = new ConstantCallSite(noop); + } + return callSite; + } + + // package visibility for testing + static MethodHandle generateNoopMethodHandle(MethodType methodType) { + Class returnType = methodType.returnType(); + MethodHandle noopNoArg; + if (returnType == void.class) { + noopNoArg = + MethodHandles.constant(Void.class, null).asType(MethodType.methodType(void.class)); + } else { + noopNoArg = MethodHandles.constant(returnType, getDefaultValue(returnType)); + } + return MethodHandles.dropArguments(noopNoArg, 0, methodType.parameterList()); + } + + private static Object getDefaultValue(Class classOrPrimitive) { + if (classOrPrimitive.isPrimitive()) { + // arrays of primitives are initialized with the correct primitive default value (e.g. 0 for + // int.class) + // we use this fact to generate the correct default value reflectively + return Array.get(Array.newInstance(classOrPrimitive, 1), 0); + } else { + return null; // null is the default value for reference types + } + } +} diff --git a/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java b/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java new file mode 100644 index 0000000000..54b0b0938b --- /dev/null +++ b/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import org.junit.jupiter.api.Test; + +public class IndyBootstrapDispatcherTest { + + @Test + void testVoidNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(void.class)); + noArg.invokeExact(); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(void.class, int.class)); + intArg.invokeExact(42); + } + + @Test + void testIntNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(int.class)); + assertThat((int) noArg.invokeExact()).isEqualTo(0); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(int.class, int.class)); + assertThat((int) intArg.invokeExact(42)).isEqualTo(0); + } + + @Test + void testBooleanNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(boolean.class)); + assertThat((boolean) noArg.invokeExact()).isEqualTo(false); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(boolean.class, int.class)); + assertThat((boolean) intArg.invokeExact(42)).isEqualTo(false); + } + + @Test + void testReferenceNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(Runnable.class)); + assertThat((Runnable) noArg.invokeExact()).isEqualTo(null); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(Runnable.class, int.class)); + assertThat((Runnable) intArg.invokeExact(42)).isEqualTo(null); + } + + private static MethodHandle generateAndCheck(MethodType type) { + MethodHandle mh = IndyBootstrapDispatcher.generateNoopMethodHandle(type); + assertThat(mh.type()).isEqualTo(type); + return mh; + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java index 981b3345a1..8f538c2132 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java @@ -20,6 +20,9 @@ import io.opentelemetry.javaagent.tooling.bytebuddy.LoggingFailSafeMatcher; import io.opentelemetry.javaagent.tooling.config.AgentConfig; import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstaller; import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstallerFactory; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyModuleRegistry; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyTypeTransformerImpl; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.PatchByteCodeVersionTransformer; import io.opentelemetry.javaagent.tooling.muzzle.HelperResourceBuilderImpl; import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle; import io.opentelemetry.javaagent.tooling.util.IgnoreFailedTypeMatcher; @@ -62,6 +65,52 @@ public final class InstrumentationModuleInstaller { FINE, "Instrumentation {0} is disabled", instrumentationModule.instrumentationName()); return parentAgentBuilder; } + + if (instrumentationModule.isIndyModule()) { + return installIndyModule(instrumentationModule, parentAgentBuilder); + } else { + return installInjectingModule(instrumentationModule, parentAgentBuilder, config); + } + } + + private AgentBuilder installIndyModule( + InstrumentationModule instrumentationModule, AgentBuilder parentAgentBuilder) { + + IndyModuleRegistry.registerIndyModule(instrumentationModule); + + // TODO (Jonas): Adapt MuzzleMatcher to use the same type lookup strategy as the + // InstrumentationModuleClassLoader + // MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config); + VirtualFieldImplementationInstaller contextProvider = + virtualFieldInstallerFactory.create(instrumentationModule); + + AgentBuilder agentBuilder = parentAgentBuilder; + for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) { + AgentBuilder.Identified.Extendable extendableAgentBuilder = + setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation) + .transform(new PatchByteCodeVersionTransformer()); + + IndyTypeTransformerImpl typeTransformer = + new IndyTypeTransformerImpl(extendableAgentBuilder, instrumentationModule); + typeInstrumentation.transform(typeTransformer); + extendableAgentBuilder = typeTransformer.getAgentBuilder(); + // TODO (Jonas): make instrumentation of bytecode older than 1.4 opt-in via a config option + // TODO (Jonas): we are not calling + // contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder) anymore + // As a result the advices should store `VirtualFields` as static variables instead of having + // the lookup inline + // We need to update our documentation on that + extendableAgentBuilder = contextProvider.injectFields(extendableAgentBuilder); + + agentBuilder = extendableAgentBuilder; + } + return agentBuilder; + } + + private AgentBuilder installInjectingModule( + InstrumentationModule instrumentationModule, + AgentBuilder parentAgentBuilder, + ConfigProperties config) { List helperClassNames = InstrumentationModuleMuzzle.getHelperClassNames(instrumentationModule); HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl(); @@ -78,8 +127,6 @@ public final class InstrumentationModuleInstaller { return parentAgentBuilder; } - ElementMatcher.Junction moduleClassLoaderMatcher = - instrumentationModule.classLoaderMatcher(); MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config); AgentBuilder.Transformer helperInjector = new HelperInjector( @@ -93,32 +140,9 @@ public final class InstrumentationModuleInstaller { AgentBuilder agentBuilder = parentAgentBuilder; for (TypeInstrumentation typeInstrumentation : typeInstrumentations) { - ElementMatcher typeMatcher = - new NamedMatcher<>( - instrumentationModule.getClass().getSimpleName() - + "#" - + typeInstrumentation.getClass().getSimpleName(), - new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher())); - ElementMatcher classLoaderMatcher = - new NamedMatcher<>( - instrumentationModule.getClass().getSimpleName() - + "#" - + typeInstrumentation.getClass().getSimpleName(), - moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization())); AgentBuilder.Identified.Extendable extendableAgentBuilder = - agentBuilder - .type( - new LoggingFailSafeMatcher<>( - typeMatcher, - "Instrumentation type matcher unexpected exception: " + typeMatcher), - new LoggingFailSafeMatcher<>( - classLoaderMatcher, - "Instrumentation class loader matcher unexpected exception: " - + classLoaderMatcher)) - .and( - (typeDescription, classLoader, module, classBeingRedefined, protectionDomain) -> - classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription)) + setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation) .and(muzzleMatcher) .transform(ConstantAdjuster.instance()) .transform(helperInjector); @@ -133,4 +157,37 @@ public final class InstrumentationModuleInstaller { return agentBuilder; } + + private static AgentBuilder.Identified.Narrowable setTypeMatcher( + AgentBuilder agentBuilder, + InstrumentationModule instrumentationModule, + TypeInstrumentation typeInstrumentation) { + + ElementMatcher.Junction moduleClassLoaderMatcher = + instrumentationModule.classLoaderMatcher(); + + ElementMatcher typeMatcher = + new NamedMatcher<>( + instrumentationModule.getClass().getSimpleName() + + "#" + + typeInstrumentation.getClass().getSimpleName(), + new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher())); + ElementMatcher classLoaderMatcher = + new NamedMatcher<>( + instrumentationModule.getClass().getSimpleName() + + "#" + + typeInstrumentation.getClass().getSimpleName(), + moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization())); + + return agentBuilder + .type( + new LoggingFailSafeMatcher<>( + typeMatcher, "Instrumentation type matcher unexpected exception: " + typeMatcher), + new LoggingFailSafeMatcher<>( + classLoaderMatcher, + "Instrumentation class loader matcher unexpected exception: " + classLoaderMatcher)) + .and( + (typeDescription, classLoader, module, classBeingRedefined, protectionDomain) -> + classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription)); + } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java new file mode 100644 index 0000000000..94c12ee89b --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.bootstrap.IndyBootstrapDispatcher; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.utility.JavaConstant; + +/** + * We instruct Byte Buddy (via {@link Advice.WithCustomMapping#bootstrap(java.lang.reflect.Method)}) + * to dispatch {@linkplain Advice.OnMethodEnter#inline() non-inlined advices} via an invokedynamic + * (indy) instruction. The target method is linked to a dynamically created instrumentation module + * class loader that is specific to an instrumentation module and the class loader of the + * instrumented method. + * + *

The first invocation of an {@code INVOKEDYNAMIC} causes the JVM to dynamically link a {@link + * CallSite}. In this case, it will use the {@link #bootstrap} method to do that. This will also + * create the {@link InstrumentationModuleClassLoader}. + * + *

+ *
+ *   Bootstrap CL ←──────────────────────────── Agent CL
+ *       ↑ └───────── IndyBootstrapDispatcher ─ ↑ ──→ └────────────── {@link IndyBootstrap#bootstrap}
+ *     Ext/Platform CL               ↑          │                        ╷
+ *       ↑                           ╷          │                        ↓
+ *     System CL                     ╷          │        {@link IndyModuleRegistry#getInstrumentationClassloader(String, ClassLoader)}
+ *       ↑                           ╷          │                        ╷
+ *     Common               linking of CallSite │                        ╷
+ *     ↑    ↑             (on first invocation) │                        ╷
+ * WebApp1  WebApp2                  ╷          │                     creates
+ *          ↑ - InstrumentedClass    ╷          │                        ╷
+ *          │                ╷       ╷          │                        ╷
+ *          │                INVOKEDYNAMIC      │                        ↓
+ *          └────────────────┼──────────────────{@link InstrumentationModuleClassLoader}
+ *                           └╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶→├ AdviceClass
+ *                                                  ├ AdviceHelper
+ *                                                  └ {@link LookupExposer}
+ *
+ * Legend:
+ *  ╶╶→ method calls
+ *  ──→ class loader parent/child relationships
+ * 
+ */ +public class IndyBootstrap { + + private static final Logger logger = Logger.getLogger(IndyBootstrap.class.getName()); + + private static final Method indyBootstrapMethod; + + private static final CallDepth callDepth = CallDepth.forClass(IndyBootstrap.class); + + static { + try { + indyBootstrapMethod = + IndyBootstrapDispatcher.class.getMethod( + "bootstrap", + MethodHandles.Lookup.class, + String.class, + MethodType.class, + Object[].class); + + MethodType bootstrapMethodType = + MethodType.methodType( + ConstantCallSite.class, + MethodHandles.Lookup.class, + String.class, + MethodType.class, + Object[].class); + + IndyBootstrapDispatcher.init( + MethodHandles.lookup().findStatic(IndyBootstrap.class, "bootstrap", bootstrapMethodType)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private IndyBootstrap() {} + + public static Method getIndyBootstrapMethod() { + return indyBootstrapMethod; + } + + @Nullable + @SuppressWarnings({"unused", "removal"}) // SecurityManager and AccessController are deprecated + private static ConstantCallSite bootstrap( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType adviceMethodType, + Object[] args) { + + if (System.getSecurityManager() == null) { + return internalBootstrap(lookup, adviceMethodName, adviceMethodType, args); + } + + // callsite resolution needs privileged access to call Class#getClassLoader() and + // MethodHandles$Lookup#findStatic + return java.security.AccessController.doPrivileged( + (PrivilegedAction) + () -> internalBootstrap(lookup, adviceMethodName, adviceMethodType, args)); + } + + private static ConstantCallSite internalBootstrap( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType adviceMethodType, + Object[] args) { + try { + if (callDepth.getAndIncrement() > 0) { + // avoid re-entrancy and stack overflow errors, which may happen when bootstrapping an + // instrumentation that also gets triggered during the bootstrap + // for example, adding correlation ids to the thread context when executing logger.debug. + logger.log( + Level.WARNING, + "Nested instrumented invokedynamic instruction linkage detected", + new Throwable()); + return null; + } + // See the getAdviceBootstrapArguments method for where these arguments come from + String moduleClassName = (String) args[0]; + String adviceClassName = (String) args[1]; + + InstrumentationModuleClassLoader instrumentationClassloader = + IndyModuleRegistry.getInstrumentationClassloader( + moduleClassName, lookup.lookupClass().getClassLoader()); + + // Advices are not inlined. They are loaded as normal classes by the + // InstrumentationModuleClassloader and invoked via a method call from the instrumented method + Class adviceClass = instrumentationClassloader.loadClass(adviceClassName); + MethodHandle methodHandle = + instrumentationClassloader + .getLookup() + .findStatic(adviceClass, adviceMethodName, adviceMethodType); + return new ConstantCallSite(methodHandle); + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + return null; + } finally { + callDepth.decrementAndGet(); + } + } + + static Advice.BootstrapArgumentResolver.Factory getAdviceBootstrapArguments( + InstrumentationModule instrumentationModule) { + String moduleName = instrumentationModule.getClass().getName(); + return (adviceMethod, exit) -> + (instrumentedType, instrumentedMethod) -> + Arrays.asList( + JavaConstant.Simple.ofLoaded(moduleName), + JavaConstant.Simple.ofLoaded(adviceMethod.getDeclaringType().getName())); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java new file mode 100644 index 0000000000..1048abcf5a --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.instrumentation.api.internal.cache.Cache; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle; +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class IndyModuleRegistry { + + private IndyModuleRegistry() {} + + private static final ConcurrentHashMap modulesByName = + new ConcurrentHashMap<>(); + + /** + * Weakly references the {@link InstrumentationModuleClassLoader}s for a given application + * classloader. We only store weak references to make sure we don't prevent application + * classloaders from being GCed. The application classloaders will strongly reference the {@link + * InstrumentationModuleClassLoader} through the invokedynamic callsites. + */ + private static final ConcurrentHashMap< + InstrumentationModule, + Cache>> + instrumentationClassloaders = new ConcurrentHashMap<>(); + + public static InstrumentationModuleClassLoader getInstrumentationClassloader( + String moduleClassName, ClassLoader instrumentedClassloader) { + InstrumentationModule instrumentationModule = modulesByName.get(moduleClassName); + if (instrumentationModule == null) { + throw new IllegalArgumentException( + "No module with the class name " + modulesByName + " has been registered!"); + } + return getInstrumentationClassloader(instrumentationModule, instrumentedClassloader); + } + + private static synchronized InstrumentationModuleClassLoader getInstrumentationClassloader( + InstrumentationModule module, ClassLoader instrumentedClassloader) { + + Cache> cacheForModule = + instrumentationClassloaders.computeIfAbsent(module, (k) -> Cache.weak()); + + WeakReference cached = + cacheForModule.get(instrumentedClassloader); + if (cached != null) { + InstrumentationModuleClassLoader cachedCl = cached.get(); + if (cachedCl != null) { + return cachedCl; + } + } + // We can't directly use "compute-if-absent" here because then for a short time only the + // WeakReference will point to the InstrumentationModuleCL + InstrumentationModuleClassLoader created = + createInstrumentationModuleClassloader(module, instrumentedClassloader); + cacheForModule.put(instrumentedClassloader, new WeakReference<>(created)); + return created; + } + + private static InstrumentationModuleClassLoader createInstrumentationModuleClassloader( + InstrumentationModule module, ClassLoader instrumentedClassloader) { + + Set toInject = new HashSet<>(InstrumentationModuleMuzzle.getHelperClassNames(module)); + // TODO (Jonas): Make muzzle include advice classes as helper classes + // so that we don't have to include them here + toInject.addAll(getModuleAdviceNames(module)); + + ClassLoader agentOrExtensionCl = module.getClass().getClassLoader(); + Map injectedClasses = + toInject.stream() + .collect( + Collectors.toMap( + name -> name, name -> ClassCopySource.create(name, agentOrExtensionCl))); + + return new InstrumentationModuleClassLoader( + instrumentedClassloader, agentOrExtensionCl, injectedClasses); + } + + public static void registerIndyModule(InstrumentationModule module) { + if (!module.isIndyModule()) { + throw new IllegalArgumentException("Provided module is not an indy module!"); + } + String moduleName = module.getClass().getName(); + if (modulesByName.putIfAbsent(moduleName, module) != null) { + throw new IllegalArgumentException( + "A module with the class name " + moduleName + " has already been registered!"); + } + } + + private static Set getModuleAdviceNames(InstrumentationModule module) { + Set adviceNames = new HashSet<>(); + TypeTransformer nameCollector = + new TypeTransformer() { + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + adviceNames.add(adviceClassName); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) {} + }; + for (TypeInstrumentation instr : module.typeInstrumentations()) { + instr.transform(nameCollector); + } + return adviceNames; + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java new file mode 100644 index 0000000000..d4059e383e --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public final class IndyTypeTransformerImpl implements TypeTransformer { + private final Advice.WithCustomMapping adviceMapping; + private AgentBuilder.Identified.Extendable agentBuilder; + private final InstrumentationModule instrumentationModule; + + public IndyTypeTransformerImpl( + AgentBuilder.Identified.Extendable agentBuilder, InstrumentationModule module) { + this.agentBuilder = agentBuilder; + this.instrumentationModule = module; + this.adviceMapping = + Advice.withCustomMapping() + .with(new Advice.AssignReturned.Factory().withSuppressed(Throwable.class)) + .bootstrap( + IndyBootstrap.getIndyBootstrapMethod(), + IndyBootstrap.getAdviceBootstrapArguments(instrumentationModule)); + } + + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + agentBuilder = + agentBuilder.transform( + new AgentBuilder.Transformer.ForAdvice(adviceMapping) + .advice(methodMatcher, adviceClassName) + .include(instrumentationModule.getClass().getClassLoader()) + .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler())); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) { + agentBuilder = agentBuilder.transform(transformer); + } + + public AgentBuilder.Identified.Extendable getAgentBuilder() { + return agentBuilder; + } +} diff --git a/testing-common/integration-tests/build.gradle.kts b/testing-common/integration-tests/build.gradle.kts index d503cf2b9b..7e74bd26df 100644 --- a/testing-common/integration-tests/build.gradle.kts +++ b/testing-common/integration-tests/build.gradle.kts @@ -45,10 +45,27 @@ tasks { jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } + val testIndyModuleOldBytecodeInstrumentation by registering(Test::class) { + filter { + includeTestsMatching("InstrumentOldBytecode") + } + include("**/InstrumentOldBytecode.*") + jvmArgs("-Dotel.instrumentation.inline-ibm-resource-level.enabled=false") + } + + val testInlineModuleOldBytecodeInstrumentation by registering(Test::class) { + filter { + includeTestsMatching("InstrumentOldBytecode") + } + include("**/InstrumentOldBytecode.*") + jvmArgs("-Dotel.instrumentation.indy-ibm-resource-level.enabled=false") + } + 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/main/java/IndyIbmResourceLevelInstrumentationModule.java b/testing-common/integration-tests/src/main/java/IndyIbmResourceLevelInstrumentationModule.java new file mode 100644 index 0000000000..9dee76b8ac --- /dev/null +++ b/testing-common/integration-tests/src/main/java/IndyIbmResourceLevelInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class IndyIbmResourceLevelInstrumentationModule extends InstrumentationModule { + public IndyIbmResourceLevelInstrumentationModule() { + super("indy-ibm-resource-level"); + } + + @Override + public boolean isIndyModule() { + return true; + } + + @Override + public List typeInstrumentations() { + return singletonList(new IndyResourceLevelInstrumentation()); + } +} diff --git a/testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java b/testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java new file mode 100644 index 0000000000..d66fe5b9a9 --- /dev/null +++ b/testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class IndyResourceLevelInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.ibm.as400.resource.ResourceLevel"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("toString"), this.getClass().getName() + "$ToStringAdvice"); + } + + @SuppressWarnings("unused") + public static class ToStringAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, inline = false) + @Advice.AssignReturned.ToReturned + public static String toStringReplace() { + return "instrumented"; + } + } +} diff --git a/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java b/testing-common/integration-tests/src/main/java/InlineIbmResourceLevelInstrumentationModule.java similarity index 65% rename from testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java rename to testing-common/integration-tests/src/main/java/InlineIbmResourceLevelInstrumentationModule.java index 518ec9c585..4e06e733b6 100644 --- a/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java +++ b/testing-common/integration-tests/src/main/java/InlineIbmResourceLevelInstrumentationModule.java @@ -11,13 +11,13 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import java.util.List; @AutoService(InstrumentationModule.class) -public class IbmResourceLevelInstrumentationModule extends InstrumentationModule { - public IbmResourceLevelInstrumentationModule() { - super(IbmResourceLevelInstrumentationModule.class.getName()); +public class InlineIbmResourceLevelInstrumentationModule extends InstrumentationModule { + public InlineIbmResourceLevelInstrumentationModule() { + super("inline-ibm-resource-level"); } @Override public List typeInstrumentations() { - return singletonList(new ResourceLevelInstrumentation()); + return singletonList(new InlineResourceLevelInstrumentation()); } } diff --git a/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java b/testing-common/integration-tests/src/main/java/InlineResourceLevelInstrumentation.java similarity index 92% rename from testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java rename to testing-common/integration-tests/src/main/java/InlineResourceLevelInstrumentation.java index e362a06128..1e4f074e94 100644 --- a/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java +++ b/testing-common/integration-tests/src/main/java/InlineResourceLevelInstrumentation.java @@ -11,7 +11,7 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -public class ResourceLevelInstrumentation implements TypeInstrumentation { +public class InlineResourceLevelInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { return named("com.ibm.as400.resource.ResourceLevel"); diff --git a/testing-common/integration-tests/src/main/java/indy/IndyInstrumentationTestModule.java b/testing-common/integration-tests/src/main/java/indy/IndyInstrumentationTestModule.java new file mode 100644 index 0000000000..3b1a831d93 --- /dev/null +++ b/testing-common/integration-tests/src/main/java/indy/IndyInstrumentationTestModule.java @@ -0,0 +1,176 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package indy; + +import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; +import net.bytebuddy.asm.Advice.AssignReturned.ToFields.ToField; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class IndyInstrumentationTestModule extends InstrumentationModule { + + public IndyInstrumentationTestModule() { + super("indy-test"); + } + + @Override + public boolean isIndyModule() { + return true; + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new Instrumentation()); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals(LocalHelper.class.getName()); + } + + public static class Instrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("indy.IndyInstrumentationTest"); + } + + @Override + public void transform(TypeTransformer transformer) { + String prefix = getClass().getName(); + transformer.applyAdviceToMethod( + named("assignToFieldViaReturn"), prefix + "$AssignFieldViaReturnAdvice"); + transformer.applyAdviceToMethod( + named("assignToFieldViaArray"), prefix + "$AssignFieldViaArrayAdvice"); + transformer.applyAdviceToMethod( + named("assignToArgumentViaReturn"), prefix + "$AssignArgumentViaReturnAdvice"); + transformer.applyAdviceToMethod( + named("assignToArgumentViaArray"), prefix + "$AssignArgumentViaArrayAdvice"); + transformer.applyAdviceToMethod( + named("assignToReturnViaReturn"), prefix + "$AssignReturnViaReturnAdvice"); + transformer.applyAdviceToMethod( + named("assignToReturnViaArray"), prefix + "$AssignReturnViaArrayAdvice"); + transformer.applyAdviceToMethod(named("getHelperClass"), prefix + "$GetHelperClassAdvice"); + transformer.applyAdviceToMethod(named("exceptionPlease"), prefix + "$ThrowExceptionAdvice"); + transformer.applyAdviceToMethod( + named("noExceptionPlease"), prefix + "$SuppressExceptionAdvice"); + } + + @SuppressWarnings({"unused"}) + public static class AssignFieldViaReturnAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToFields(@ToField(value = "privateField")) + public static String onEnter(@Advice.Argument(0) String toAssign) { + return toAssign; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignFieldViaArrayAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToFields(@ToField(value = "privateField", index = 1, typing = DYNAMIC)) + public static Object[] onEnter(@Advice.Argument(0) String toAssign) { + return new Object[] {"ignoreme", toAssign}; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignArgumentViaReturnAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static String onEnter(@Advice.Argument(1) String toAssign) { + return toAssign; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignArgumentViaArrayAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToArguments(@ToArgument(value = 0, index = 1, typing = DYNAMIC)) + public static Object[] onEnter(@Advice.Argument(1) String toAssign) { + return new Object[] {"ignoreme", toAssign}; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignReturnViaReturnAdvice { + + @Advice.OnMethodExit(inline = false) + @Advice.AssignReturned.ToReturned + public static String onExit(@Advice.Argument(0) String toAssign) { + return toAssign; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignReturnViaArrayAdvice { + + @Advice.OnMethodExit(inline = false) + @Advice.AssignReturned.ToReturned(index = 1, typing = DYNAMIC) + public static Object[] onExit(@Advice.Argument(0) String toAssign) { + return new Object[] {"ignoreme", toAssign}; + } + } + + @SuppressWarnings({"unused"}) + public static class GetHelperClassAdvice { + + @Advice.OnMethodExit(inline = false) + @Advice.AssignReturned.ToReturned + public static Class onExit(@Advice.Argument(0) boolean localHelper) { + if (localHelper) { + return LocalHelper.class; + } else { + return GlobalHelper.class; + } + } + } + + @SuppressWarnings({"unused", "ThrowSpecificExceptions"}) + public static class ThrowExceptionAdvice { + @Advice.OnMethodExit(inline = false) + public static void onMethodExit() { + throw new RuntimeException("This exception should not be suppressed"); + } + } + + @SuppressWarnings({"unused", "ThrowSpecificExceptions"}) + public static class SuppressExceptionAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static void onMethodEnter() { + throw new RuntimeException("This exception should be suppressed"); + } + + @Advice.AssignReturned.ToReturned + @Advice.OnMethodExit( + suppress = Throwable.class, + onThrowable = Throwable.class, + inline = false) + public static void onMethodExit(@Advice.Thrown Throwable throwable) { + throw new RuntimeException("This exception should be suppressed"); + } + } + } + + public static class GlobalHelper {} + + public static class LocalHelper {} +} diff --git a/testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java b/testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java new file mode 100644 index 0000000000..6d87267c50 --- /dev/null +++ b/testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package indy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"unused", "MethodCanBeStatic"}) +public class IndyInstrumentationTest { + + private String privateField; + + // The following methods are instrumented by the IndyInstrumentationTestModule + + private void assignToFieldViaReturn(String toAssign) {} + + private void assignToFieldViaArray(String toAssign) {} + + private String assignToArgumentViaReturn(String a, String toAssign) { + return "Arg:" + a; + } + + private String assignToArgumentViaArray(String a, String toAssign) { + return "Arg:" + a; + } + + private String assignToReturnViaReturn(String toAssign) { + return "replace_me"; + } + + private String assignToReturnViaArray(String toAssign) { + return "replace_me"; + } + + private String noExceptionPlease(String s) { + return s + "_no_exception"; + } + + private void exceptionPlease() {} + + private Class getHelperClass(boolean local) { + return null; + } + + @AfterEach + public void reset() { + privateField = null; + } + + @Test + void testAssignToFieldViaReturn() { + assignToFieldViaReturn("field_val"); + assertThat(privateField).isEqualTo("field_val"); + } + + @Test + void testAssignToFieldViaArray() { + assignToFieldViaArray("array_field_val"); + assertThat(privateField).isEqualTo("array_field_val"); + } + + @Test + void testAssignToArgumentViaReturn() { + String value = assignToArgumentViaReturn("", "arg_val"); + assertThat(value).isEqualTo("Arg:arg_val"); + } + + @Test + void testAssignToArgumentViaArray() { + String value = assignToArgumentViaArray("", "arg_array_val"); + assertThat(value).isEqualTo("Arg:arg_array_val"); + } + + @Test + void testAssignToReturnViaReturn() { + String value = assignToReturnViaReturn("ret_val"); + assertThat(value).isEqualTo("ret_val"); + } + + @Test + void testAssignToReturnViaArray() { + String value = assignToReturnViaReturn("ret_array_val"); + assertThat(value).isEqualTo("ret_array_val"); + } + + @Test + void testSuppressException() { + assertThat(noExceptionPlease("foo")).isEqualTo("foo_no_exception"); + } + + @Test + void testThrowExceptionIntoUserCode() { + assertThatThrownBy(this::exceptionPlease).isInstanceOf(RuntimeException.class); + } + + @Test + void testHelperClassLoading() { + Class localHelper = getHelperClass(true); + assertThat(localHelper.getName()).endsWith("LocalHelper"); + assertThat(localHelper.getClassLoader().getClass().getName()) + .endsWith("InstrumentationModuleClassLoader"); + + Class globalHelper = getHelperClass(false); + assertThat(globalHelper.getName()).endsWith("GlobalHelper"); + assertThat(globalHelper.getClassLoader().getClass().getName()).endsWith("AgentClassLoader"); + } +}