diff --git a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java index 82f1b2214b7..481e27b225d 100644 --- a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java +++ b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java @@ -9,8 +9,6 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.none; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.environment.EnvironmentVariables; import de.thetaphi.forbiddenapis.SuppressForbidden; @@ -18,6 +16,7 @@ import java.lang.instrument.Instrumentation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; @@ -40,7 +39,11 @@ * JUnit 5 extension that manages DD config injection for tests. Handles: * *
{@code sun.misc.Unsafe} is accessed entirely via reflection so the module can keep the + * default {@code --release} compile setting (the internal {@code sun.misc} package would + * otherwise be off-limits to the compiler). + */ + @SuppressForbidden + private static final class UnsafeFieldWriter { + private static Object unsafe; + private static Method staticFieldBase; + private static Method staticFieldOffset; + private static Method putObjectVolatile; + + private final Object base; + private final long offset; + + private UnsafeFieldWriter(Object base, long offset) { + this.base = base; + this.offset = offset; + } + + /** + * @throws ReflectiveOperationException if {@code sun.misc.Unsafe} or one of the required + * methods is unavailable on this JVM. Lets the caller mark config modification as failed + * and surface a clean test failure instead of a {@link ExceptionInInitializerError}. + */ + static UnsafeFieldWriter forStaticField(Field staticField) throws ReflectiveOperationException { + ensureInitialized(); + Object fieldBase = staticFieldBase.invoke(unsafe, staticField); + long fieldOffset = (long) staticFieldOffset.invoke(unsafe, staticField); + return new UnsafeFieldWriter(fieldBase, fieldOffset); + } + + void putVolatile(Object value) { + try { + putObjectVolatile.invoke(unsafe, base, offset, value); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to write static field via Unsafe", e); + } + } + + private static synchronized void ensureInitialized() throws ReflectiveOperationException { + if (unsafe != null) { + return; + } + Class> unsafeClass = Class.forName("sun.misc.Unsafe"); + Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + Object instance = theUnsafe.get(null); + staticFieldBase = unsafeClass.getMethod("staticFieldBase", Field.class); + staticFieldOffset = unsafeClass.getMethod("staticFieldOffset", Field.class); + putObjectVolatile = + unsafeClass.getMethod("putObjectVolatile", Object.class, long.class, Object.class); + // Publish `unsafe` last so a partially-initialized state can't fool the early-return guard. + unsafe = instance; + } + } + // endregion // region Property management @@ -316,26 +405,6 @@ static void restoreProperties() { // endregion - // region Validation - - private static void checkConfigTransformation() { - assertTrue(isConfigInstanceModifiable); - assertNotNull(instConfigConstructor); - checkWritable(instConfigInstanceField); - assertNotNull(configConstructor); - checkWritable(configInstanceField); - } - - private static void checkWritable(Field field) { - assertNotNull(field); - assertTrue(Modifier.isPublic(field.getModifiers())); - assertTrue(Modifier.isStatic(field.getModifiers())); - assertTrue(Modifier.isVolatile(field.getModifiers())); - assertFalse(Modifier.isFinal(field.getModifiers())); - } - - // endregion - /** Test-only environment variable provider that replaces the real one during tests. */ public static class TestEnvironmentVariables extends EnvironmentVariables.EnvironmentVariablesProvider { @@ -393,7 +462,10 @@ public void onError( JavaModule module, boolean loaded, @NonNull Throwable throwable) { - if (CONFIG.equals(typeName)) { + if (INST_CONFIG.equals(typeName) || CONFIG.equals(typeName)) { + // Note: this only marks failure for ByteBuddy errors that surface as listener errors. + // Silent retransformation failures (IBM J9 / Semeru) are detected later in + // makeConfigInstanceModifiable() by inspecting the actual field modifiers. configModificationFailed = true; } }