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

Align use of extension point #496

Merged
merged 33 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4a26fdb
Simplify system property names in tests
Jun 1, 2021
cbf5445
Clarify entry-based extension code re extension points
Jun 1, 2021
1d99ac3
Make entry-based extension tests thread-safe
Jun 1, 2021
864c62c
Remove outdated comment
beatngu13 Jun 1, 2021
f3060b4
Rename nested classes
beatngu13 Jun 1, 2021
48a051b
Add resetting tests
beatngu13 Jun 1, 2021
32f49bb
Change default locale/time zone extension points
Jun 1, 2021
6a474c3
Document general approach in CONTRIBUTING.md
beatngu13 Sep 21, 2021
a7b7c28
Documenting extension scopes in respective documentations
beatngu13 Sep 21, 2021
ddcf8c4
Remove explanation of extension scope
Sep 23, 2021
398bca3
Improve descreasoning in contribution guide
Sep 23, 2021
06fa9ee
Restructure DefaultLocale tests of (re)setting behavior
Sep 23, 2021
8158152
Remove redundant tests
Nov 14, 2021
93670e5
Clarify behavior of class-level SysProp/EnvVar extensions
Nov 14, 2021
1873ab6
Merge branch 'main' into issue/479-extension-points-annotations
beatngu13 Mar 12, 2022
c09215c
Fix key
beatngu13 Mar 13, 2022
05571b3
Add util method to find all (parent) extension contexts
beatngu13 Mar 13, 2022
3330767
Apply all entry-based extensions per method
beatngu13 Mar 13, 2022
b76c469
Revert "Change default locale/time zone extension points"
beatngu13 Mar 13, 2022
9dd7446
Sync resetting test names
beatngu13 Mar 13, 2022
cbb0fc4
Update contributing guide
beatngu13 Mar 14, 2022
6ef0e12
Update Javadoc
beatngu13 Mar 14, 2022
04ab5b8
Update docs
beatngu13 Mar 14, 2022
30cca6e
Time zones are evil
beatngu13 Mar 14, 2022
62cdb7c
Mention order in Javadoc
beatngu13 Mar 14, 2022
c36dddb
Inline variable and document application order
beatngu13 Mar 14, 2022
f9cd9d0
¯\_(ツ)_/¯
beatngu13 Mar 14, 2022
3208c00
Simplify method
beatngu13 Mar 14, 2022
4774a0e
Mention callbacks in contributing guide
beatngu13 Mar 15, 2022
a9fd42d
Improve comment
beatngu13 Mar 18, 2022
5f8a579
Improve documentation
Mar 24, 2022
9a08971
Document what lifecycle methods observe
Mar 24, 2022
9d13fee
Nicolai is stupid
Mar 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,35 @@ Classes implementing an extension's functionality should reflect that in their n

Note _should_, not _must_ - there can be exceptions if well argued.

#### Extension Scopes

Consider the following:

```java
@YourExtension
class MyTests {

@Test
void testFoo() { /* ... */ }

@Test
void testBar() { /* ... */ }

}
```

You might ask yourself: does `@YourExtension` run

1. once before/after all tests (meaning it "brackets" the test class) or
2. once before/after each test (meaning it "brackets" each test method)?

We decided to _default_ to option 2 as we believe this is less error-prone and covers more common use cases.
Furthermore, we want to guarantee consistent behavior across different extensions.
(Examples are e.g. `DefaultLocaleExtension` or `DefaultTimezoneExtension`.)

This, however, is just a default.
`@YourExtension` is free to diverge if it makes sense.

#### Namespaces

Interacting with [Jupiter's extension `Store`](https://junit.org/junit5/docs/current/user-guide/#extensions-keeping-state) requires a `Namespace` instance.
Expand Down
4 changes: 4 additions & 0 deletions docs/default-locale-timezone.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ class MyLocaleTests {
}
----

Note that class level configurations mean that the corresponding locale is set before every test inside the annotated class.

== `@DefaultTimeZone`

The default `TimeZone` is specified according to the https://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html#getTimeZone(java.lang.String)[TimeZone.getTimeZone(String)] method.
Expand Down Expand Up @@ -121,6 +123,8 @@ class MyTimeZoneTests {
}
----

Note that class level configurations mean that the corresponding time zone is set before every test inside the annotated class.

== Thread-Safety

Since default locale and time zone are global state, reading and writing them during https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution[parallel test execution] can lead to unpredictable results and flaky tests.
Expand Down
2 changes: 2 additions & 0 deletions docs/environment-variables.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class MyEnvironmentVariableTest {
}
----

Also note that class level configurations mean that the corresponding environment variables are cleared/set before every test inside the annotated class.

== Warnings for Reflective Access

As explained above, this extension uses reflective access to change the otherwise immutable environment variables.
Expand Down
2 changes: 2 additions & 0 deletions docs/system-properties.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class MySystemPropertyTest {
}
----

Also note that class level configurations mean that the corresponding system properties are cleared/set before every test inside 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.
In this case, you can still leverage the restore mechanism:

Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/junitpioneer/internal/PioneerUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.stream.Collector;
import java.util.stream.Collectors;

import org.junit.jupiter.api.extension.ExtensionContext;

/**
* Pioneer-internal utility class.
* DO NOT USE THIS CLASS - IT MAY CHANGE SIGNIFICANTLY IN ANY MINOR UPDATE.
Expand Down Expand Up @@ -82,6 +84,23 @@ public static Optional<Method> findMethodCurrentOrEnclosing(Class<?> clazz, Stri
return method;
}

/**
* Find all (parent) {@code ExtensionContext}s via {@link ExtensionContext#getParent()}.
*
* @param context the context for which to find all (parent) contexts; never {@code null}
* @return a list of all contexts, beginning with the given context; never {@code null} or empty
*/
public static List<ExtensionContext> findAllContexts(ExtensionContext context) {
List<ExtensionContext> allContexts = new ArrayList<>();
allContexts.add(context);
List<ExtensionContext> parentContexts = context
.getParent()
.map(PioneerUtils::findAllContexts)
.orElse(Collections.emptyList());
allContexts.addAll(parentContexts);
return allContexts;
}

public static String nullSafeToString(Object object) {
if (object == null) {
return "null";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@

package org.junitpioneer.jupiter;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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 @@ -43,60 +45,52 @@
* @param <S> The set annotation type.
*/
abstract class AbstractEntryBasedExtension<K, V, C extends Annotation, S extends Annotation>
implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback, AfterEachCallback {

@Override
public void beforeAll(ExtensionContext context) {
clearAndSetEntries(context);
}
implements BeforeEachCallback, AfterEachCallback {

@Override
public void beforeEach(ExtensionContext context) {
clearAndSetEntries(context);
List<ExtensionContext> contexts = PioneerUtils.findAllContexts(context);
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved
// apply from outermost to innermost
Collections.reverse(contexts);
contexts.forEach(this::clearAndSetEntries);
}

private void clearAndSetEntries(ExtensionContext context) {
Set<K> entriesToClear;
Map<K, V> entriesToSet;

try {
entriesToClear = findEntriesToClear(context);
entriesToSet = findEntriesToSet(context);
preventClearAndSetSameEntries(entriesToClear, entriesToSet.keySet());
}
catch (IllegalStateException ex) {
throw new ExtensionConfigurationException("Don't clear/set the same entry more than once.", ex);
}

if (entriesToClear.isEmpty() && entriesToSet.isEmpty())
return;

reportWarning(context);
storeOriginalEntries(context, entriesToClear, entriesToSet.keySet());
clearEntries(entriesToClear);
setEntries(entriesToSet);
}

private Set<K> findEntriesToClear(ExtensionContext context) {
return findAnnotations(context, getClearAnnotationType())
context.getElement().ifPresent(element -> {
Set<K> entriesToClear;
Map<K, V> entriesToSet;

try {
entriesToClear = findEntriesToClear(element);
entriesToSet = findEntriesToSet(element);
preventClearAndSetSameEntries(entriesToClear, entriesToSet.keySet());
}
catch (IllegalStateException ex) {
throw new ExtensionConfigurationException("Don't clear/set the same entry more than once.", ex);
}

if (entriesToClear.isEmpty() && entriesToSet.isEmpty())
return;

reportWarning(context);
storeOriginalEntries(context, entriesToClear, entriesToSet.keySet());
clearEntries(entriesToClear);
setEntries(entriesToSet);
});
}

private Set<K> findEntriesToClear(AnnotatedElement element) {
return findAnnotations(element, getClearAnnotationType())
.map(clearKeyMapper())
.collect(PioneerUtils.distinctToSet());
}

private Map<K, V> findEntriesToSet(ExtensionContext context) {
return findAnnotations(context, getSetAnnotationType()).collect(toMap(setKeyMapper(), setValueMapper()));
private Map<K, V> findEntriesToSet(AnnotatedElement element) {
return findAnnotations(element, getSetAnnotationType()).collect(toMap(setKeyMapper(), setValueMapper()));
}

private <A extends Annotation> Stream<A> findAnnotations(ExtensionContext context, Class<A> clazz) {
/*
* Implementation notes:
*
* This extension implements `BeforeAllCallback` and `BeforeEachCallback` and so if an outer class (i.e. a
* class that the test class is @Nested within) uses this extension, this method will be called on those
* extension points and discover the variables to set/clear. That means we don't need to search for
* enclosing annotations here.
*/
return AnnotationSupport.findRepeatableAnnotations(context.getElement(), clazz).stream();
private <A extends Annotation> Stream<A> findAnnotations(AnnotatedElement element, Class<A> clazz) {
return AnnotationSupport.findRepeatableAnnotations(element, clazz).stream();
}

private Class<C> getClearAnnotationType() {
Expand All @@ -113,15 +107,14 @@ private <T> Class<T> getActualTypeArgumentAt(int index) {
}

private void preventClearAndSetSameEntries(Collection<K> entriesToClear, Collection<K> entriesToSet) {
entriesToClear
String duplicateEntries = entriesToClear
.stream()
.filter(entriesToSet::contains)
.map(Object::toString)
.reduce((e0, e1) -> e0 + ", " + e1)
.ifPresent(duplicateEntries -> {
throw new IllegalStateException(
"Cannot clear and set the following entries at the same time: " + duplicateEntries);
});
.collect(joining(", "));
if (!duplicateEntries.isEmpty())
throw new IllegalStateException(
"Cannot clear and set the following entries at the same time: " + duplicateEntries);
}

private void storeOriginalEntries(ExtensionContext context, Collection<K> entriesToClear,
Expand All @@ -139,12 +132,8 @@ private void setEntries(Map<K, V> entriesToSet) {

@Override
public void afterEach(ExtensionContext context) throws Exception {
restoreOriginalEntries(context);
}

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

private void restoreOriginalEntries(ExtensionContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* <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.</p>
* cleared before every test inside that class.</p>
*
* <p>WARNING: Java considers environment variables to be immutable, so this extension
* uses reflection to change them. This requires that the {@link SecurityManager}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* <p>{@code ClearSystemProperty} is repeatable and can be used on the method and
* on the class level. If a class is annotated, the configured property will be
* cleared for all tests inside that class.</p>
* cleared before every test inside that class.</p>
*
* <p>During
* <a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*
* <p>{@code SetEnvironmentVariable} is repeatable and can be used on the method and on
* the class level. If a class is annotated, the configured variable will be set
* for all tests inside that class. Any method level configurations will
* before every test inside that 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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*
* <p>{@code SetSystemProperty} is repeatable and can be used on the method and on
* the class level. If a class is annotated, the configured property will be set
* for all tests inside that class. Any method level configurations will
* before every test inside that class. Any method level configurations will
* override the class level configurations.</p>
*
* <p>During
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ void tearDown() {

@Test
@Issue("432")
@WritesSystemProperty
@WritesEnvironmentVariable
@DisplayName("should not mix backups of different extensions on clear environment variable and clear system property")
void shouldNotMixBackupsOfDifferentExtensionsOnClearEnvironmentVariableAndClearSystemProperty() {
PioneerTestKit.executeTestMethod(MixBackupsTestCases.class, "clearEnvironmentVariableAndClearSystemProperty");
Expand All @@ -58,6 +60,8 @@ void shouldNotMixBackupsOfDifferentExtensionsOnClearEnvironmentVariableAndClearS

@Test
@Issue("432")
@WritesSystemProperty
@WritesEnvironmentVariable
@DisplayName("should not mix backups of different extensions on set environment variable and set system property")
void shouldNotMixBackupsOfDifferentExtensionsOnSetEnvironmentVariableAndSetSystemProperty() {
PioneerTestKit.executeTestMethod(MixBackupsTestCases.class, "setEnvironmentVariableAndSetSystemProperty");
Expand All @@ -68,6 +72,8 @@ void shouldNotMixBackupsOfDifferentExtensionsOnSetEnvironmentVariableAndSetSyste

@Test
@Issue("432")
@WritesSystemProperty
@WritesEnvironmentVariable
@DisplayName("should not mix backups of different extensions on clear environment variable and set system property")
void shouldNotMixBackupsOfDifferentExtensionsOnClearEnvironmentVariableAndSetSystemProperty() {
PioneerTestKit.executeTestMethod(MixBackupsTestCases.class, "clearEnvironmentVariableAndSetSystemProperty");
Expand All @@ -78,6 +84,8 @@ void shouldNotMixBackupsOfDifferentExtensionsOnClearEnvironmentVariableAndSetSys

@Test
@Issue("432")
@WritesSystemProperty
@WritesEnvironmentVariable
@DisplayName("should not mix backups of different extensions on set environment variable and clear system property")
void shouldNotMixBackupsOfDifferentExtensionsOnSetEnvironmentVariableAndClearSystemProperty() {
PioneerTestKit.executeTestMethod(MixBackupsTestCases.class, "setEnvironmentVariableAndClearSystemProperty");
Expand Down