Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new annotations ClearEnvironmentVariable and SetEnvironmentVariable.
- Loading branch information
1 parent
c8a82b2
commit 4e3d0c5
Showing
10 changed files
with
726 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
:page-title: @ClearEnvironmentVariable and @SetEnvironmentVariable | ||
:page-description: JUnit Jupiter extensions to clear and set the values of environment variable | ||
|
||
The `@ClearEnvironmentVariable` and `@SetEnvironmentVariable` annotations can be used to clear, respectively, set the values of environment variables for a test execution. | ||
Both annotations work on the test method and class level, are repeatable as well as combinable. | ||
After the annotated method has been executed, the variables mentioned in the annotation will be restored to their original value or will be cleared if they didn't have one before. | ||
Other environment variables that are changed during the test, are *not* restored. | ||
|
||
For example, clearing a environment variable for a test execution can be done as follows: | ||
|
||
[source,java] | ||
---- | ||
@Test | ||
@ClearEnvironmentVariable(key = "some variable") | ||
void test() { | ||
assertThat(System.getenv("some variable")).isNull(); | ||
} | ||
---- | ||
|
||
And setting a environment variable for a test execution: | ||
|
||
[source,java] | ||
---- | ||
@Test | ||
@SetEnvironmentVariable(key = "some variable", value = "new value") | ||
void test() { | ||
assertThat(System.getenv("some variable")).isEqualsTo("new value"); | ||
} | ||
---- | ||
|
||
As mentioned before, both annotations are repeatable and they can also be combined: | ||
|
||
[source,java] | ||
---- | ||
@Test | ||
@ClearEnvironmentVariable(key = "1st variable") | ||
@ClearEnvironmentVariable(key = "2nd variable") | ||
@SetEnvironmentVariable(key = "3rd variable", value = "new value") | ||
void test() { | ||
assertThat(System.getenv("1st variable")).isNull(); | ||
assertThat(System.getenv("2nd variable")).isNull(); | ||
assertThat(System.getenv("3rd variable")).isEqualsTo("new value"); | ||
} | ||
---- | ||
|
||
Note that class level configurations are overwritten by method level configurations: | ||
|
||
[source,java] | ||
---- | ||
@ClearEnvironmentVariable(key = "some variable") | ||
class MySystemPropertyTest { | ||
@Test | ||
@SetEnvironmentVariable(key = "some variable", value = "new value") | ||
void test() { | ||
assertThat(System.getenv("some variable")).isEqualsTo("new value"); | ||
} | ||
} | ||
---- |
46 changes: 46 additions & 0 deletions
46
src/main/java/org/junitpioneer/jupiter/ClearEnvironmentVariable.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* Copyright 2015-2020 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import java.lang.annotation.ElementType; | ||
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; | ||
|
||
/** | ||
* {@code @ClearEnvironmentVariable} is a JUnit Jupiter extension to clear the value | ||
* of a environment variable for a test execution. | ||
* | ||
* <p>The key of the environment variable to be cleared must be specified via | ||
* {@link #key()}. After the annotated element has been executed, After the | ||
* annotated method has been executed, the initial default value is restored. | ||
* | ||
* <p>{@code ClearEnvironmentVariable} is repeatable and can be used on the method and | ||
* on the class level. If a class is annotated, the configured variable will be | ||
* cleared for all tests inside that class. | ||
* | ||
* @since 0.6 | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ ElementType.METHOD, ElementType.TYPE }) | ||
@Repeatable(ClearEnvironmentVariables.class) | ||
@ExtendWith(EnvironmentVariableExtension.class) | ||
public @interface ClearEnvironmentVariable { | ||
|
||
/** | ||
* The key of the environment variable to be cleared. | ||
*/ | ||
String key(); | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/org/junitpioneer/jupiter/ClearEnvironmentVariables.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright 2015-2020 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
import org.junit.jupiter.api.extension.ExtendWith; | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ ElementType.METHOD, ElementType.TYPE }) | ||
@ExtendWith(EnvironmentVariableExtension.class) | ||
public @interface ClearEnvironmentVariables { | ||
|
||
ClearEnvironmentVariable[] value(); | ||
|
||
} |
140 changes: 140 additions & 0 deletions
140
src/main/java/org/junitpioneer/jupiter/EnvironmentVariableExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/* | ||
* Copyright 2015-2020 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import static java.util.stream.Collectors.*; | ||
|
||
import java.lang.annotation.Annotation; | ||
import java.util.*; | ||
import java.util.stream.Stream; | ||
|
||
import org.junit.jupiter.api.extension.AfterAllCallback; | ||
import org.junit.jupiter.api.extension.AfterEachCallback; | ||
import org.junit.jupiter.api.extension.BeforeAllCallback; | ||
import org.junit.jupiter.api.extension.BeforeEachCallback; | ||
import org.junit.jupiter.api.extension.ExtensionConfigurationException; | ||
import org.junit.jupiter.api.extension.ExtensionContext; | ||
import org.junit.jupiter.api.extension.ExtensionContext.Namespace; | ||
import org.junit.platform.commons.support.AnnotationSupport; | ||
|
||
class EnvironmentVariableExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback, AfterEachCallback { | ||
|
||
private static final Namespace NAMESPACE = Namespace.create(EnvironmentVariableExtension.class); | ||
private static final String BACKUP = "Backup"; | ||
|
||
@Override | ||
public void beforeAll(ExtensionContext context) { | ||
handleEnvironmentVariables(context); | ||
} | ||
|
||
@Override | ||
public void beforeEach(ExtensionContext context) { | ||
boolean present = Utils.annotationPresentOnTestMethod(context, ClearEnvironmentVariable.class, | ||
ClearEnvironmentVariables.class, SetEnvironmentVariable.class, SetEnvironmentVariables.class); | ||
if (present) { | ||
handleEnvironmentVariables(context); | ||
} | ||
} | ||
|
||
private void handleEnvironmentVariables(ExtensionContext context) { | ||
Set<String> variablesToClear; | ||
Map<String, String> variablesToSet; | ||
try { | ||
variablesToClear = findRepeatableAnnotations(context, ClearEnvironmentVariable.class).stream().map( | ||
ClearEnvironmentVariable::key).collect(Utils.distinctToSet()); | ||
variablesToSet = findRepeatableAnnotations(context, SetEnvironmentVariable.class).stream().collect( | ||
toMap(SetEnvironmentVariable::key, SetEnvironmentVariable::value)); | ||
preventClearAndSetSameEnvironmentVariables(variablesToClear, variablesToSet.keySet()); | ||
} catch (IllegalStateException ex) { | ||
throw new ExtensionConfigurationException("Don't clear/set the same environment variable more than once.", ex); | ||
} | ||
|
||
storeOriginalEnvironmentVariables(context, variablesToClear, variablesToSet.keySet()); | ||
EnvironmentVariableUtils.clear(variablesToClear); | ||
EnvironmentVariableUtils.set(variablesToSet); | ||
} | ||
|
||
private void preventClearAndSetSameEnvironmentVariables(Collection<String> variablesToClear, | ||
Collection<String> variablesToSet) { | ||
// @formatter:off | ||
variablesToClear.stream() | ||
.filter(variablesToSet::contains) | ||
.reduce((k0, k1) -> k0 + ", " + k1) | ||
.ifPresent(duplicateKeys -> { | ||
throw new IllegalStateException( | ||
"Cannot clear and set the following environment variable at the same time: " + duplicateKeys); | ||
}); | ||
// @formatter:on | ||
} | ||
|
||
private <A extends Annotation> List<A> findRepeatableAnnotations(ExtensionContext context, | ||
Class<A> annotationType) { | ||
// @formatter:off | ||
return context.getElement() | ||
.map(element -> AnnotationSupport.findRepeatableAnnotations(element, annotationType)) | ||
.orElseGet(Collections::emptyList); | ||
// @formatter:on | ||
} | ||
|
||
private void storeOriginalEnvironmentVariables(ExtensionContext context, Collection<String> clearVariables, | ||
Collection<String> setVariables) { | ||
context.getStore(NAMESPACE).put(BACKUP, new EnvironmentVariableBackup(clearVariables, setVariables)); | ||
} | ||
|
||
@Override | ||
public void afterEach(ExtensionContext context) { | ||
boolean present = Utils.annotationPresentOnTestMethod(context, ClearEnvironmentVariable.class, | ||
ClearEnvironmentVariables.class, SetEnvironmentVariable.class, SetEnvironmentVariables.class); | ||
if (present) { | ||
restoreOriginalEnvironmentVariables(context); | ||
} | ||
} | ||
|
||
@Override | ||
public void afterAll(ExtensionContext context) { | ||
restoreOriginalEnvironmentVariables(context); | ||
} | ||
|
||
private void restoreOriginalEnvironmentVariables(ExtensionContext context) { | ||
context.getStore(NAMESPACE).get(BACKUP, EnvironmentVariableBackup.class).restoreVariables(); | ||
} | ||
|
||
/** | ||
* Stores which environment variables need to be cleared or set to their old values after the test. | ||
*/ | ||
private static class EnvironmentVariableBackup { | ||
|
||
private final Map<String, String> variablesToSet; | ||
private final Set<String> variablesToUnset; | ||
|
||
public EnvironmentVariableBackup(Collection<String> clearVariables, Collection<String> setVariables) { | ||
variablesToSet = new HashMap<>(); | ||
variablesToUnset = new HashSet<>(); | ||
// @formatter:off | ||
Stream.concat(clearVariables.stream(), setVariables.stream()) | ||
.forEach(variable -> { | ||
String backup = System.getenv(variable); | ||
if (backup == null) | ||
variablesToUnset.add(variable); | ||
else | ||
variablesToSet.put(variable, backup); | ||
}); | ||
// @formatter:on | ||
} | ||
|
||
public void restoreVariables() { | ||
EnvironmentVariableUtils.set(variablesToSet); | ||
EnvironmentVariableUtils.clear(variablesToUnset); | ||
} | ||
|
||
} | ||
|
||
} |
106 changes: 106 additions & 0 deletions
106
src/main/java/org/junitpioneer/jupiter/EnvironmentVariableUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* Copyright 2015-2020 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import java.lang.reflect.Field; | ||
import java.util.Collection; | ||
import java.util.Map; | ||
import java.util.function.Consumer; | ||
|
||
/** | ||
* This class modifies the internals of the environment variables map with reflection. | ||
* If your {@link SecurityManager} does not allow modifications, it fails. | ||
*/ | ||
public class EnvironmentVariableUtils { | ||
|
||
/** | ||
* Set the values of an environment variables. | ||
* | ||
* @param entries with name and new value of the environment variables | ||
*/ | ||
public static void set(Map<String, String> entries) { | ||
modifyEnvironmentVariables(map -> map.putAll(entries)); | ||
} | ||
|
||
/** | ||
* Clears environment variables. | ||
* | ||
* @param names of the environment variables. | ||
*/ | ||
public static void clear(Collection<String> names) { | ||
modifyEnvironmentVariables(map -> names.forEach(map::remove)); | ||
} | ||
|
||
/** | ||
* Set a value of an environment variable. | ||
* | ||
* @param name of the environment variable | ||
* @param value of the environment variable | ||
*/ | ||
public static void set(String name, String value) { | ||
modifyEnvironmentVariables(map -> map.put(name, value)); | ||
} | ||
|
||
/** | ||
* Clear an environment variable. | ||
* | ||
* @param name of the environment variable | ||
*/ | ||
public static void clear(String name) { | ||
modifyEnvironmentVariables(map -> map.remove(name)); | ||
} | ||
|
||
private static void modifyEnvironmentVariables(Consumer<Map<String, String>> consumer) { | ||
try { | ||
tryProcessEnvironmentClassFallbackSystemEnvClass(consumer); | ||
} catch (NoSuchFieldException e) { | ||
throw new RuntimeException("Could not modify environment variables"); | ||
} | ||
} | ||
|
||
private static void tryProcessEnvironmentClassFallbackSystemEnvClass(Consumer<Map<String, String>> consumer) | ||
throws NoSuchFieldException { | ||
try { | ||
setInProcessEnvironmentClass(consumer); | ||
} catch (NoSuchFieldException | ClassNotFoundException e) { | ||
setInSystemEnvClass(consumer); | ||
} | ||
} | ||
|
||
/* | ||
* Works on Windows | ||
*/ | ||
private static void setInProcessEnvironmentClass(Consumer<Map<String, String>> consumer) | ||
throws ClassNotFoundException, NoSuchFieldException { | ||
Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment"); | ||
consumer.accept(getFieldValue(processEnvironmentClass, "theEnvironment")); | ||
consumer.accept(getFieldValue(processEnvironmentClass, "theCaseInsensitiveEnvironment")); | ||
} | ||
|
||
/* | ||
* Works on Linux | ||
*/ | ||
private static void setInSystemEnvClass(Consumer<Map<String, String>> consumer) throws NoSuchFieldException { | ||
consumer.accept(getFieldValue(System.getenv().getClass(), "m")); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private static Map<String, String> getFieldValue(Class<?> clazz, String name) throws NoSuchFieldException { | ||
Field field = clazz.getDeclaredField(name); | ||
try { | ||
field.setAccessible(true); | ||
return (Map<String, String>) field.get(null); | ||
} catch (IllegalAccessException e) { | ||
throw new RuntimeException("Cannot access field " + clazz.getName() + "." + name, e); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.