From e749671c888b252d53f16382ef5912e1365451ac Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 10 Apr 2026 19:44:30 +0200 Subject: [PATCH 1/5] feat(junit): Fix @WithConfig to be applied at any level --- .../trace/junit/utils/config/WithConfig.java | 10 +++++---- .../utils/config/WithConfigExtension.java | 21 +++++++++++++------ .../trace/junit/utils/config/WithConfigs.java | 10 +++++---- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java index 172c24f03ff..a77045210ca 100644 --- a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java +++ b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java @@ -1,9 +1,11 @@ package datadog.trace.junit.utils.config; -import java.lang.annotation.ElementType; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,8 +29,8 @@ * } * } */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RUNTIME) +@Target({TYPE, METHOD}) @Repeatable(WithConfigs.class) @ExtendWith(WithConfigExtension.class) public @interface WithConfig { 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 5041d8dc669..23865c35517 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 @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -109,12 +110,20 @@ public void afterAll(ExtensionContext context) { } private void applyDeclaredConfig(ExtensionContext context) { - // Class-level @WithConfig annotations (supports composed/meta-annotations) - List classConfigs = - AnnotationSupport.findRepeatableAnnotations( - context.getRequiredTestClass(), WithConfig.class); - for (WithConfig cfg : classConfigs) { - applyConfig(cfg); + // Class-level @WithConfig annotations + // Walk the entire class hierarchy so annotations on superclasses are applied + // (topmost first, then subclass overrides) + Class testClass = context.getRequiredTestClass(); + List> hierarchy = new ArrayList<>(); + for (Class cls = testClass; cls != null; cls = cls.getSuperclass()) { + hierarchy.add(cls); + } + for (int i = hierarchy.size() - 1; i >= 0; i--) { + List classConfigs = + AnnotationSupport.findRepeatableAnnotations(hierarchy.get(i), WithConfig.class); + for (WithConfig cfg : classConfigs) { + applyConfig(cfg); + } } // Method-level @WithConfig annotations (supports composed/meta-annotations) context diff --git a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java index 64a21fe5845..e0343437823 100644 --- a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java +++ b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java @@ -1,14 +1,16 @@ package datadog.trace.junit.utils.config; -import java.lang.annotation.ElementType; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.extension.ExtendWith; /** Container annotation for repeatable {@link WithConfig}. */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RUNTIME) +@Target({TYPE, METHOD}) @ExtendWith(WithConfigExtension.class) public @interface WithConfigs { WithConfig[] value(); From 8f6e54bc3977cff9f78406611b163761eaaf9e2b Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Mon, 13 Apr 2026 13:44:46 +0200 Subject: [PATCH 2/5] fix(junit): Only rebuild config once --- .../utils/config/WithConfigExtension.java | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) 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 23865c35517..6f801222522 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 @@ -63,6 +63,7 @@ public class WithConfigExtension private static Field configInstanceField; private static Constructor configConstructor; + private static volatile boolean configTransformerInstalled = false; private static volatile boolean isConfigInstanceModifiable = false; private static volatile boolean configModificationFailed = false; @@ -74,9 +75,15 @@ public class WithConfigExtension @Override public void beforeAll(ExtensionContext context) { - installConfigTransformer(); + if (!configTransformerInstalled) { + installConfigTransformer(); + configTransformerInstalled = true; + } makeConfigInstanceModifiable(); assertFalse(configModificationFailed, "Config class modification failed"); + if (isConfigInstanceModifiable) { + checkConfigTransformation(); + } if (originalSystemProperties == null) { saveProperties(); } @@ -86,10 +93,10 @@ public void beforeAll(ExtensionContext context) { public void beforeEach(ExtensionContext context) { restoreProperties(); environmentVariables.clear(); + applyDeclaredConfig(context); if (isConfigInstanceModifiable) { rebuildConfig(); } - applyDeclaredConfig(context); } @Override @@ -140,12 +147,22 @@ private void applyDeclaredConfig(ExtensionContext context) { private static void applyConfig(WithConfig cfg) { if (cfg.env()) { - injectEnvConfig(cfg.key(), cfg.value(), cfg.addPrefix()); + setEnvVariable(cfg.key(), cfg.value(), cfg.addPrefix()); } else { - injectSysConfig(cfg.key(), cfg.value(), cfg.addPrefix()); + setSysProperty(cfg.key(), cfg.value(), cfg.addPrefix()); } } + private static void setSysProperty(String name, String value, boolean addPrefix) { + String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name; + System.setProperty(prefixedName, value); + } + + private static void setEnvVariable(String name, String value, boolean addPrefix) { + String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name; + environmentVariables.set(prefixedName, value); + } + // endregion // region Public static API for imperative config injection @@ -155,9 +172,7 @@ public static void injectSysConfig(String name, String value) { } public static void injectSysConfig(String name, String value, boolean addPrefix) { - checkConfigTransformation(); - String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name; - System.setProperty(prefixedName, value); + setSysProperty(name, value, addPrefix); rebuildConfig(); } @@ -166,7 +181,6 @@ public static void removeSysConfig(String name) { } public static void removeSysConfig(String name, boolean addPrefix) { - checkConfigTransformation(); String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name; System.clearProperty(prefixedName); rebuildConfig(); @@ -177,9 +191,7 @@ public static void injectEnvConfig(String name, String value) { } public static void injectEnvConfig(String name, String value, boolean addPrefix) { - checkConfigTransformation(); - String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name; - environmentVariables.set(prefixedName, value); + setEnvVariable(name, value, addPrefix); rebuildConfig(); } @@ -188,7 +200,6 @@ public static void removeEnvConfig(String name) { } public static void removeEnvConfig(String name, boolean addPrefix) { - checkConfigTransformation(); String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name; environmentVariables.removePrefixed(prefixedName); rebuildConfig(); @@ -254,7 +265,6 @@ static void makeConfigInstanceModifiable() { private static void rebuildConfig() { synchronized (WithConfigExtension.class) { - checkConfigTransformation(); try { Object newInstConfig = instConfigConstructor.newInstance(); instConfigInstanceField.set(null, newInstConfig); From b37c48cec1464adc798ed2078cd4ea57a8f97d65 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Mon, 13 Apr 2026 14:13:01 +0200 Subject: [PATCH 3/5] fix(junit): Apply class level configuration at @BeforeEach level So child classes can benefits from class level configs and we can configure the tracer for example --- .../junit/utils/config/WithConfigExtension.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 6f801222522..28d175ee0f1 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 @@ -87,6 +87,11 @@ public void beforeAll(ExtensionContext context) { if (originalSystemProperties == null) { saveProperties(); } + // Apply class-level @WithConfig so config is available before @BeforeAll methods + applyClassLevelConfig(context); + if (isConfigInstanceModifiable) { + rebuildConfig(); + } } @Override @@ -116,8 +121,12 @@ public void afterAll(ExtensionContext context) { } } - private void applyDeclaredConfig(ExtensionContext context) { - // Class-level @WithConfig annotations + private static void applyDeclaredConfig(ExtensionContext context) { + applyClassLevelConfig(context); + applyMethodLevelConfig(context); + } + + private static void applyClassLevelConfig(ExtensionContext context) { // Walk the entire class hierarchy so annotations on superclasses are applied // (topmost first, then subclass overrides) Class testClass = context.getRequiredTestClass(); @@ -132,6 +141,9 @@ private void applyDeclaredConfig(ExtensionContext context) { applyConfig(cfg); } } + } + + private static void applyMethodLevelConfig(ExtensionContext context) { // Method-level @WithConfig annotations (supports composed/meta-annotations) context .getTestMethod() From 75368628489010f6dc967b078b5f356c2bd2d7b8 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 17 Apr 2026 13:25:16 +0200 Subject: [PATCH 4/5] feat(test): Extract config cleanup assertions into dedicated JUnit extension Move DD_* environment variable and dd.* system property validation logic from DDJavaSpecification into a reusable CleanConfigStateExtension. Improves error reporting with detailed leak information. --- .../test/util/CleanConfigStateExtension.java | 80 +++++++++++++++++++ .../trace/test/util/DDJavaSpecification.java | 34 ++------ 2 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 utils/test-utils/src/main/java/datadog/trace/test/util/CleanConfigStateExtension.java diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/CleanConfigStateExtension.java b/utils/test-utils/src/main/java/datadog/trace/test/util/CleanConfigStateExtension.java new file mode 100644 index 00000000000..09c18ced5a4 --- /dev/null +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/CleanConfigStateExtension.java @@ -0,0 +1,80 @@ +package datadog.trace.test.util; + +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; + +import datadog.environment.EnvironmentVariables; +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Asserts that no {@code DD_*} environment variable and no {@code dd.*} system property (minus a + * small allowlist) is set around a test class. + */ +@SuppressForbidden +public class CleanConfigStateExtension implements BeforeAllCallback, AfterAllCallback { + + private static final List ALLOWED_SYS_PROPS = + Arrays.asList( + "dd.appsec.enabled", "dd.iast.enabled", "dd.integration.grizzly-filterchain.enabled"); + + private static final Predicate DATADOG_ENV_VAR_FILTER = k -> k.startsWith("DD_"); + private static final Predicate DATADOG_SYS_PROPERTIES_FILTER = + o -> { + String key = (String) o; + return key.startsWith("DD_") && !ALLOWED_SYS_PROPS.contains(key); + }; + + @Override + public void beforeAll(ExtensionContext context) { + assertClean("before"); + } + + @Override + public void afterAll(ExtensionContext context) { + assertClean("after"); + } + + private static void assertClean(String phase) { + Map leakedEnv = + filterMap(EnvironmentVariables.getAll(), DATADOG_ENV_VAR_FILTER); + Map leakedSys = + filterMap(System.getProperties(), DATADOG_SYS_PROPERTIES_FILTER); + if (!leakedEnv.isEmpty() || !leakedSys.isEmpty()) { + assertionFailure() + .message("Leaked Datadog configuration detected " + phase + " test class") + .reason(formatLeaks(leakedEnv, leakedSys)) + .buildAndThrow(); + } + } + + private static Map filterMap(Map map, Predicate keyFilter) { + return map.entrySet().stream() + .filter(e -> keyFilter.test(e.getKey())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a, b) -> a, TreeMap::new)); + } + + private static String formatLeaks(Map env, Map sys) { + StringBuilder sb = new StringBuilder(); + if (!env.isEmpty()) { + sb.append("environment variables:"); + env.forEach((k, v) -> sb.append("\n ").append(k).append('=').append(v)); + } + if (!sys.isEmpty()) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append("system properties:"); + sys.forEach((k, v) -> sb.append("\n ").append(k).append('=').append(v)); + } + return sb.toString(); + } +} diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/DDJavaSpecification.java b/utils/test-utils/src/main/java/datadog/trace/test/util/DDJavaSpecification.java index 44fd75c7327..ed1d508aec3 100644 --- a/utils/test-utils/src/main/java/datadog/trace/test/util/DDJavaSpecification.java +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/DDJavaSpecification.java @@ -1,14 +1,9 @@ package datadog.trace.test.util; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import datadog.environment.EnvironmentVariables; import datadog.trace.junit.utils.config.WithConfigExtension; import datadog.trace.junit.utils.context.AllowContextTestingExtension; import de.thetaphi.forbiddenapis.SuppressForbidden; -import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.AfterAll; @@ -16,7 +11,11 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith({WithConfigExtension.class, AllowContextTestingExtension.class}) +@ExtendWith({ + CleanConfigStateExtension.class, + WithConfigExtension.class, + AllowContextTestingExtension.class +}) @SuppressForbidden public class DDJavaSpecification { @@ -27,13 +26,6 @@ public class DDJavaSpecification { @BeforeAll static void beforeAll() { - assertTrue( - EnvironmentVariables.getAll().entrySet().stream() - .noneMatch(e -> e.getKey().startsWith("DD_"))); - assertTrue( - systemPropertiesExceptAllowed().entrySet().stream() - .noneMatch(e -> e.getKey().toString().startsWith("dd."))); - if (getDDThreads().isEmpty()) { ignoreThreadCleanup = false; } else { @@ -45,25 +37,9 @@ static void beforeAll() { @AfterAll static void afterAll() { - assertTrue( - EnvironmentVariables.getAll().entrySet().stream() - .noneMatch(e -> e.getKey().startsWith("DD_"))); - assertTrue( - systemPropertiesExceptAllowed().entrySet().stream() - .noneMatch(e -> e.getKey().toString().startsWith("dd."))); - checkThreads(); } - private static Map systemPropertiesExceptAllowed() { - List allowlist = - Arrays.asList( - "dd.appsec.enabled", "dd.iast.enabled", "dd.integration.grizzly-filterchain.enabled"); - return System.getProperties().entrySet().stream() - .filter(e -> !allowlist.contains(String.valueOf(e.getKey()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - @AfterEach void cleanup() { if (assertThreadsEachCleanup) { From af45d6c21c8f5a9c49dd3f4a9776315061b20e02 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 17 Apr 2026 09:24:03 +0200 Subject: [PATCH 5/5] feat(junit): Improve generated code readability --- .../utils/config/WithConfigExtension.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) 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 28d175ee0f1..82f1b2214b7 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 @@ -75,15 +75,24 @@ public class WithConfigExtension @Override public void beforeAll(ExtensionContext context) { + /* + * Patch config classes to make them modifiable. + */ + // Install config transformer error listener if (!configTransformerInstalled) { installConfigTransformer(); configTransformerInstalled = true; } + // Make config instance modifiable makeConfigInstanceModifiable(); + // Verify that config class transformation succeeded assertFalse(configModificationFailed, "Config class modification failed"); if (isConfigInstanceModifiable) { checkConfigTransformation(); } + /* + * Back up config and apply class-level config values. + */ if (originalSystemProperties == null) { saveProperties(); } @@ -127,8 +136,8 @@ private static void applyDeclaredConfig(ExtensionContext context) { } private static void applyClassLevelConfig(ExtensionContext context) { - // Walk the entire class hierarchy so annotations on superclasses are applied - // (topmost first, then subclass overrides) + // Walk the entire class hierarchy so annotations on superclasses and apply topmost first, then + // subclass overrides. Class testClass = context.getRequiredTestClass(); List> hierarchy = new ArrayList<>(); for (Class cls = testClass; cls != null; cls = cls.getSuperclass()) { @@ -166,12 +175,12 @@ private static void applyConfig(WithConfig cfg) { } private static void setSysProperty(String name, String value, boolean addPrefix) { - String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name; + String prefixedName = addPrefix && !name.startsWith("dd.") ? "dd." + name : name; System.setProperty(prefixedName, value); } private static void setEnvVariable(String name, String value, boolean addPrefix) { - String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name; + String prefixedName = addPrefix && !name.startsWith("DD_") ? "DD_" + name : name; environmentVariables.set(prefixedName, value); } @@ -193,7 +202,7 @@ public static void removeSysConfig(String name) { } public static void removeSysConfig(String name, boolean addPrefix) { - String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name; + String prefixedName = addPrefix && !name.startsWith("dd.") ? "dd." + name : name; System.clearProperty(prefixedName); rebuildConfig(); } @@ -212,7 +221,7 @@ public static void removeEnvConfig(String name) { } public static void removeEnvConfig(String name, boolean addPrefix) { - String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name; + String prefixedName = addPrefix && !name.startsWith("DD_") ? "DD_" + name : name; environmentVariables.removePrefixed(prefixedName); rebuildConfig(); }