Skip to content
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

Make entry-based extensions execute around classes #641

Merged
merged 11 commits into from Oct 6, 2022
6 changes: 4 additions & 2 deletions docs/environment-variables.adoc
Expand Up @@ -5,7 +5,7 @@

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, combinable, and inherited from higher-level containers.
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.
After the annotated method has been executed, the variables mentioned in the annotation will be restored to their original value or the value of the higher-level container, or will be cleared if they didn't have one before.
Other environment variables that are changed during the test, are *not* restored.

WARNING: Java considers environment variables to be immutable, so this extension uses reflection to change them.
Expand Down Expand Up @@ -34,13 +34,15 @@ As mentioned before, both annotations are repeatable and they can also be combin
include::{demo}[tag=environment_using_set_and_clear]
----

Note that class level configurations are overwritten by method level configurations:
Note that class-level configurations are overwritten by method-level configurations:

[source,java,indent=0]
----
include::{demo}[tag=environment_using_at_class_level]
----

NOTE: Method-level configurations are visible in both `@BeforeEach` setup methods and `@AfterEach` teardown methods (see https://junit.org/junit5/docs/current/user-guide/#extensions-execution-order-overview[user code and extension code execution order]).

NOTE: Since v1.7.0, a class-level configuration means that the specified environment variables are cleared/set before and reset after each individual test in the annotated class.

== Warnings for Reflective Access
Expand Down
6 changes: 4 additions & 2 deletions docs/system-properties.adoc
Expand Up @@ -5,7 +5,7 @@

The `@ClearSystemProperty` and `@SetSystemProperty` annotations can be used to clear, respectively, set the values of system properties for a test execution.
Both annotations work on the test method and class level, are repeatable, combinable, and inherited from higher-level containers.
After the annotated method has been executed, the properties mentioned in the annotation will be restored to their original value or will be cleared if they didn't have one before.
After the annotated method has been executed, the properties mentioned in the annotation will be restored to their original value or the value of the higher-level container, or will be cleared if they didn't have one before.
Other system properties that are changed during the test, are *not* restored.

For example, clearing a system property for a test execution can be done as follows:
Expand All @@ -30,13 +30,15 @@ include::{demo}[tag=systemproperty_using_set_and_clear]

----

Note that class level configurations are overwritten by method level configurations:
Note that class-level configurations are overwritten by method-level configurations:

[source,java,indent=0]
----
include::{demo}[tag=systemproperty_using_at_class_level]
----

NOTE: Method-level configurations are visible in both `@BeforeEach` setup methods and `@AfterEach` teardown methods (see https://junit.org/junit5/docs/current/user-guide/#extensions-execution-order-overview[user code and extension code execution order]).

NOTE: Since v1.7.0, a class-level configuration means that the specified system properties are cleared/set before and reset after each individual test in the annotated class.

Sometimes, you might also need to set a system property to a value that is not a constant expression, which is required for annotation values.
Expand Down
Expand Up @@ -27,7 +27,9 @@
import java.util.function.Function;
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;
Expand All @@ -46,22 +48,31 @@
* @param <S> The set annotation type.
*/
abstract class AbstractEntryBasedExtension<K, V, C extends Annotation, S extends Annotation>
implements BeforeEachCallback, AfterEachCallback {
implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, AfterAllCallback {

@Override
public void beforeAll(ExtensionContext context) {
applyForAllContexts(context);
}

@Override
public void beforeEach(ExtensionContext context) {
applyForAllContexts(context);
}

private void applyForAllContexts(ExtensionContext originalContext) {
/*
* We cannot use PioneerAnnotationUtils#findAllEnclosingRepeatableAnnotations(ExtensionContext, Class) or the
* like as clearing and setting might interfere. Therefore, we have to apply the extension from the outermost
* to the innermost ExtensionContext.
*/
List<ExtensionContext> contexts = PioneerUtils.findAllContexts(context);
List<ExtensionContext> contexts = PioneerUtils.findAllContexts(originalContext);
Collections.reverse(contexts);
contexts.forEach(this::clearAndSetEntries);
contexts.forEach(currentContext -> clearAndSetEntries(currentContext, originalContext));
}

private void clearAndSetEntries(ExtensionContext context) {
context.getElement().ifPresent(element -> {
private void clearAndSetEntries(ExtensionContext currentContext, ExtensionContext originalContext) {
currentContext.getElement().ifPresent(element -> {
Set<K> entriesToClear;
Map<K, V> entriesToSet;

Expand All @@ -77,8 +88,8 @@ private void clearAndSetEntries(ExtensionContext context) {
if (entriesToClear.isEmpty() && entriesToSet.isEmpty())
return;

reportWarning(context);
storeOriginalEntries(context, entriesToClear, entriesToSet.keySet());
reportWarning(currentContext);
storeOriginalEntries(originalContext, entriesToClear, entriesToSet.keySet());
clearEntries(entriesToClear);
setEntries(entriesToSet);
});
Expand Down Expand Up @@ -138,13 +149,24 @@ private void setEntries(Map<K, V> entriesToSet) {
}

@Override
public void afterEach(ExtensionContext context) throws Exception {
// apply from innermost to outermost
PioneerUtils.findAllContexts(context).forEach(this::restoreOriginalEntries);
public void afterEach(ExtensionContext context) {
restoreForAllContexts(context);
}

@Override
public void afterAll(ExtensionContext context) {
restoreForAllContexts(context);
}

private void restoreForAllContexts(ExtensionContext originalContext) {
// restore from innermost to outermost
PioneerUtils.findAllContexts(originalContext).forEach(__ -> restoreOriginalEntries(originalContext));
}

private void restoreOriginalEntries(ExtensionContext context) {
getStore(context).getOrDefault(getStoreKey(context), EntriesBackup.class, new EntriesBackup()).restoreBackup();
private void restoreOriginalEntries(ExtensionContext originalContext) {
getStore(originalContext)
.getOrDefault(getStoreKey(originalContext), EntriesBackup.class, new EntriesBackup())
.restoreBackup();
}

private Store getStore(ExtensionContext context) {
Expand Down
Expand Up @@ -24,7 +24,8 @@
* 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, the initial default value is restored.</p>
* After the annotated element has been executed, the original value or the value of the
* higher-level container is restored.</p>
*
* <p>{@code ClearEnvironmentVariable} can be used on the method and on the class level.
* It is repeatable and inherited from higher-level containers. If a class is
Expand Down
Expand Up @@ -24,7 +24,8 @@
* of a system property for a test execution.
*
* <p>The key of the system property to be cleared must be specified via {@link #key()}.
* After the annotated element has been executed, the initial default value is restored.</p>
* After the annotated element has been executed, the original value or the value of the
* higher-level container is restored.</p>
*
* <p>{@code ClearSystemProperty} can be used on the method and on the class level.
* It is repeatable and inherited from higher-level containers. If a class is
Expand Down
Expand Up @@ -24,13 +24,13 @@
* environment variable for a test execution.
*
* <p>The key and value of the environment variable to be set must be specified via
* {@link #key()} and {@link #value()}. After the annotated method has been
* executed, the initial default value is restored.</p>
* {@link #key()} and {@link #value()}. After the annotated method has been executed,
* the original value or the value of the higher-level container is restored.</p>
*
* <p>{@code SetEnvironmentVariable} can be used on the method and on the class level.
* It is repeatable and inherited from higher-level containers. If a class is
* annotated, the configured property will be set before every test inside that
* class. Any method level configurations will override the class level
* class. Any method-level configurations will override the class-level
* configurations.</p>
*
* <p>WARNING: Java considers environment variables to be immutable, so this extension
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/org/junitpioneer/jupiter/SetSystemProperty.java
Expand Up @@ -25,12 +25,13 @@
*
* <p>The key and value of the system property to be set must be specified via
* {@link #key()} and {@link #value()}. After the annotated method has been
* executed, the initial default value is restored.</p>
* executed, the original value or the value of the higher-level container is
* restored.</p>
*
* <p>{@code SetSystemProperty} can be used on the method and on the class level.
* It is repeatable and inherited from higher-level containers. If a class is
* annotated, the configured property will be set before every test inside that
* class. Any method level configurations will override the class level
* class. Any method-level configurations will override the class-level
* configurations.</p>
*
* <p>During
Expand Down
Expand Up @@ -18,6 +18,7 @@
import static org.junitpioneer.testkit.assertion.PioneerAssert.assertThat;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
Expand Down Expand Up @@ -47,11 +48,8 @@ static void globalSetUp() {

@AfterAll
static void globalTearDown() {
assertThat(systemEnvironmentVariable("set envvar A")).isEqualTo("old A");
EnvironmentVariableUtils.clear("set envvar A");
assertThat(systemEnvironmentVariable("set envvar B")).isEqualTo("old B");
EnvironmentVariableUtils.clear("set envvar B");
assertThat(systemEnvironmentVariable("set envvar C")).isEqualTo("old C");
EnvironmentVariableUtils.clear("set envvar C");

assertThat(systemEnvironmentVariable("clear envvar D")).isNull();
Expand Down Expand Up @@ -244,7 +242,42 @@ class ResettingEnvironmentVariableTests {
@Nested
@SetEnvironmentVariable(key = "set envvar A", value = "newer A")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ResettingEnvironmentVariableNestedTests {
class ResettingEnvironmentVariableAfterEachNestedTests {

@BeforeEach
@ReadsEnvironmentVariable
void changeShouldBeVisible() {
// we already see "newest A" because BeforeEachCallBack is invoked before @BeforeEach
// see https://junit.org/junit5/docs/current/user-guide/#extensions-execution-order-overview
assertThat(System.getenv("set envvar A")).isEqualTo("newest A");
}

@Test
@SetEnvironmentVariable(key = "set envvar A", value = "newest A")
void setForTestMethod() {
assertThat(System.getenv("set envvar A")).isEqualTo("newest A");
}

@AfterEach
@ReadsEnvironmentVariable
void resetAfterTestMethodExecution() {
// we still see "newest A" because AfterEachCallBack is invoked after @AfterEach
// see https://junit.org/junit5/docs/current/user-guide/#extensions-execution-order-overview
assertThat(System.getenv("set envvar A")).isEqualTo("newest A");
}

}

@Nested
@SetEnvironmentVariable(key = "set envvar A", value = "newer A")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ResettingEnvironmentVariableAfterAllNestedTests {

@BeforeAll
@ReadsEnvironmentVariable
void changeShouldBeVisible() {
assertThat(System.getenv("set envvar A")).isEqualTo("newer A");
}

@Test
@SetEnvironmentVariable(key = "set envvar A", value = "newest A")
Expand All @@ -255,15 +288,15 @@ void setForTestMethod() {
@AfterAll
@ReadsEnvironmentVariable
void resetAfterTestMethodExecution() {
assertThat(System.getenv("set envvar A")).isEqualTo("old A");
assertThat(System.getenv("set envvar A")).isEqualTo("newer A");
}

}

@AfterAll
@ReadsEnvironmentVariable
void resetAfterTestContainerExecution() {
assertThat(System.getenv("set envvar A")).isEqualTo("old A");
assertThat(System.getenv("set envvar A")).isEqualTo("new A");
}

}
Expand Down