-
Notifications
You must be signed in to change notification settings - Fork 332
Improve Config JUnit extension #11094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e749671
8f6e54b
b37c48c
7536862
af45d6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -62,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; | ||
|
|
||
|
|
@@ -73,22 +75,42 @@ public class WithConfigExtension | |
|
|
||
| @Override | ||
| public void beforeAll(ExtensionContext context) { | ||
| installConfigTransformer(); | ||
| /* | ||
| * 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(); | ||
| } | ||
| // Apply class-level @WithConfig so config is available before @BeforeAll methods | ||
| applyClassLevelConfig(context); | ||
| if (isConfigInstanceModifiable) { | ||
| rebuildConfig(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void beforeEach(ExtensionContext context) { | ||
| restoreProperties(); | ||
| environmentVariables.clear(); | ||
| applyDeclaredConfig(context); | ||
| if (isConfigInstanceModifiable) { | ||
| rebuildConfig(); | ||
| } | ||
| applyDeclaredConfig(context); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -108,14 +130,29 @@ public void afterAll(ExtensionContext context) { | |
| } | ||
| } | ||
|
|
||
| private void applyDeclaredConfig(ExtensionContext context) { | ||
| // Class-level @WithConfig annotations (supports composed/meta-annotations) | ||
| List<WithConfig> classConfigs = | ||
| AnnotationSupport.findRepeatableAnnotations( | ||
| context.getRequiredTestClass(), WithConfig.class); | ||
| for (WithConfig cfg : classConfigs) { | ||
| applyConfig(cfg); | ||
| 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 and apply topmost first, then | ||
| // subclass overrides. | ||
| Class<?> testClass = context.getRequiredTestClass(); | ||
| List<Class<?>> 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<WithConfig> classConfigs = | ||
| AnnotationSupport.findRepeatableAnnotations(hierarchy.get(i), WithConfig.class); | ||
| for (WithConfig cfg : classConfigs) { | ||
| applyConfig(cfg); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static void applyMethodLevelConfig(ExtensionContext context) { | ||
| // Method-level @WithConfig annotations (supports composed/meta-annotations) | ||
| context | ||
| .getTestMethod() | ||
|
|
@@ -131,12 +168,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 = addPrefix && !name.startsWith("dd.") ? "dd." + name : name; | ||
| System.setProperty(prefixedName, value); | ||
| } | ||
|
|
||
| private static void setEnvVariable(String name, String value, boolean addPrefix) { | ||
| String prefixedName = addPrefix && !name.startsWith("DD_") ? "DD_" + name : name; | ||
| environmentVariables.set(prefixedName, value); | ||
| } | ||
|
Comment on lines
+177
to
+185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: why to add negation to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually wouldn't it be easier to read this way ? addPrefix && !name.startsWith("DD_") ? "DD_" + name : name;
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. That’s way more readable.
Claude generated it in the first place. The behavior was consistent so I did not changed it. Thanks for raising it though, appreciated 🙏 |
||
|
|
||
| // endregion | ||
|
|
||
| // region Public static API for imperative config injection | ||
|
|
@@ -146,9 +193,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(); | ||
| } | ||
|
|
||
|
|
@@ -157,8 +202,7 @@ public static void removeSysConfig(String name) { | |
| } | ||
|
|
||
| public static void removeSysConfig(String name, boolean addPrefix) { | ||
| checkConfigTransformation(); | ||
| String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name; | ||
| String prefixedName = addPrefix && !name.startsWith("dd.") ? "dd." + name : name; | ||
| System.clearProperty(prefixedName); | ||
| rebuildConfig(); | ||
| } | ||
|
|
@@ -168,9 +212,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(); | ||
| } | ||
|
|
||
|
|
@@ -179,8 +221,7 @@ public static void removeEnvConfig(String name) { | |
| } | ||
|
|
||
| public static void removeEnvConfig(String name, boolean addPrefix) { | ||
| checkConfigTransformation(); | ||
| String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name; | ||
| String prefixedName = addPrefix && !name.startsWith("DD_") ? "DD_" + name : name; | ||
| environmentVariables.removePrefixed(prefixedName); | ||
| rebuildConfig(); | ||
| } | ||
|
|
@@ -245,7 +286,6 @@ static void makeConfigInstanceModifiable() { | |
|
|
||
| private static void rebuildConfig() { | ||
| synchronized (WithConfigExtension.class) { | ||
| checkConfigTransformation(); | ||
| try { | ||
| Object newInstConfig = instConfigConstructor.newInstance(); | ||
| instConfigInstanceField.set(null, newInstConfig); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> ALLOWED_SYS_PROPS = | ||
| Arrays.asList( | ||
| "dd.appsec.enabled", "dd.iast.enabled", "dd.integration.grizzly-filterchain.enabled"); | ||
|
|
||
| private static final Predicate<String> DATADOG_ENV_VAR_FILTER = k -> k.startsWith("DD_"); | ||
| private static final Predicate<Object> 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<String, String> leakedEnv = | ||
| filterMap(EnvironmentVariables.getAll(), DATADOG_ENV_VAR_FILTER); | ||
| Map<Object, Object> 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 <T> Map<T, T> filterMap(Map<T, T> map, Predicate<T> 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<String, String> env, Map<Object, Object> 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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would put empty lines between
logicalblocks to make code a bit more readable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. I added comment to explain the various blocks. That should be even more explicit than empty lines.