Skip to content

Commit

Permalink
resolves #167
Browse files Browse the repository at this point in the history
Add new annotations ClearEnvironmentVariable and SetEnvironmentVariable.
  • Loading branch information
Hancho2009 authored and Nicolai Parlog committed Feb 18, 2020
1 parent c8a82b2 commit 4e3d0c5
Show file tree
Hide file tree
Showing 10 changed files with 726 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/docs-nav.yml
Expand Up @@ -4,6 +4,8 @@
children:
- title: "@ClearSystemProperty and @SetSystemProperty"
url: /docs/system-properties/
- title: "@ClearEnvironmentVariable and @SetEnvironmentVariable"
url: /docs/environment-variables/
- title: "@DefaultLocale and @DefaultTimeZone"
url: /docs/default-locale-timezone/
- title: "Range Sources"
Expand Down
58 changes: 58 additions & 0 deletions docs/enviroment-variables.adoc
@@ -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");
}
}
----
@@ -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();

}
@@ -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();

}
@@ -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 src/main/java/org/junitpioneer/jupiter/EnvironmentVariableUtils.java
@@ -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);
}
}

}

0 comments on commit 4e3d0c5

Please sign in to comment.