Skip to content

Commit 3e2cc6c

Browse files
committed
Ensure TestWatchers can access data in the ExtensionContext.Store
Changes made in conjunction with junit-team#3614 resulted in an exception being thrown if a NamespacedHierarchicalStore was queried after it had been closed. Consequently, TestWatcher callbacks were no longer able to access data in the Store. To fix that regression, this commit revises NamespacedHierarchicalStore so that it no longer throws an exception after it has been closed if the store is queried via one of the get(...) or getOrComputeIfAbsent(...) methods; however, if a getOrComputeIfAbsent(...) invocation results in the computation of a new value, an exception will still be thrown. In other words, when a NamespacedHierarchicalStore is closed, it now effectively switches to ready-only mode. See: junit-team#3614 Fixes: junit-team#3944
1 parent 1d1c6d5 commit 3e2cc6c

File tree

4 files changed

+78
-16
lines changed

4 files changed

+78
-16
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ on GitHub.
1818

1919
* Fixed potential locking issue with `ExclusiveResource` in the
2020
`HierarchicalTestExecutorService`, which could lead to deadlocks in certain scenarios.
21+
* `NamespacedHierarchicalStore` no longer throws an exception after it has been closed if
22+
the store is queried via one of the `get(...)` or `getOrComputeIfAbsent(...)` methods;
23+
however, if a `getOrComputeIfAbsent(...)` invocation results in the computation of a new
24+
value, an exception will still be thrown.
2125

2226
[[release-notes-5.11.1-junit-platform-deprecations-and-breaking-changes]]
2327
==== Deprecations and Breaking Changes
@@ -37,7 +41,8 @@ on GitHub.
3741
[[release-notes-5.11.1-junit-jupiter-bug-fixes]]
3842
==== Bug Fixes
3943

40-
* ❓
44+
* `TestWatcher` callback methods can once again access data in the
45+
`ExtensionContext.Store`.
4146

4247
[[release-notes-5.11.1-junit-jupiter-deprecations-and-breaking-changes]]
4348
==== Deprecations and Breaking Changes

junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ public void close() {
139139
* closed
140140
*/
141141
public Object get(N namespace, Object key) {
142-
rejectIfClosed();
143142
StoredValue storedValue = getStoredValue(new CompositeKey<>(namespace, key));
144143
return StoredValue.evaluateIfNotNull(storedValue);
145144
}
@@ -156,7 +155,6 @@ public Object get(N namespace, Object key) {
156155
* be cast to the required type, or if this store has already been closed
157156
*/
158157
public <T> T get(N namespace, Object key, Class<T> requiredType) throws NamespacedHierarchicalStoreException {
159-
rejectIfClosed();
160158
Object value = get(namespace, key);
161159
return castToRequiredType(key, value, requiredType);
162160
}
@@ -174,13 +172,15 @@ public <T> T get(N namespace, Object key, Class<T> requiredType) throws Namespac
174172
* closed
175173
*/
176174
public <K, V> Object getOrComputeIfAbsent(N namespace, K key, Function<K, V> defaultCreator) {
177-
rejectIfClosed();
178175
Preconditions.notNull(defaultCreator, "defaultCreator must not be null");
179176
CompositeKey<N> compositeKey = new CompositeKey<>(namespace, key);
180177
StoredValue storedValue = getStoredValue(compositeKey);
181178
if (storedValue == null) {
182179
storedValue = this.storedValues.computeIfAbsent(compositeKey,
183-
__ -> storedValue(new MemoizingSupplier(() -> defaultCreator.apply(key))));
180+
__ -> storedValue(new MemoizingSupplier(() -> {
181+
rejectIfClosed();
182+
return defaultCreator.apply(key);
183+
})));
184184
}
185185
return storedValue.evaluate();
186186
}
@@ -202,7 +202,6 @@ public <K, V> Object getOrComputeIfAbsent(N namespace, K key, Function<K, V> def
202202
public <K, V> V getOrComputeIfAbsent(N namespace, K key, Function<K, V> defaultCreator, Class<V> requiredType)
203203
throws NamespacedHierarchicalStoreException {
204204

205-
rejectIfClosed();
206205
Object value = getOrComputeIfAbsent(namespace, key, defaultCreator);
207206
return castToRequiredType(key, value, requiredType);
208207
}

jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestWatcherTests.java

+51
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static java.util.function.Predicate.not;
1414
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.assertj.core.api.Assertions.entry;
1516
import static org.junit.jupiter.api.Assertions.assertEquals;
1617
import static org.junit.jupiter.api.Assertions.assertTrue;
1718
import static org.junit.jupiter.api.Assertions.fail;
@@ -41,8 +42,11 @@
4142
import org.junit.jupiter.api.TestInstance;
4243
import org.junit.jupiter.api.TestInstance.Lifecycle;
4344
import org.junit.jupiter.api.TestMethodOrder;
45+
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
4446
import org.junit.jupiter.api.extension.ExtendWith;
4547
import org.junit.jupiter.api.extension.ExtensionContext;
48+
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
49+
import org.junit.jupiter.api.extension.ExtensionContext.Store;
4650
import org.junit.jupiter.api.extension.RegisterExtension;
4751
import org.junit.jupiter.api.extension.TestWatcher;
4852
import org.junit.jupiter.api.fixtures.TrackLogRecords;
@@ -67,6 +71,7 @@ class TestWatcherTests extends AbstractJupiterTestEngineTests {
6771
@BeforeEach
6872
void clearResults() {
6973
TrackingTestWatcher.results.clear();
74+
DataRetrievingTestWatcher.results.clear();
7075
}
7176

7277
@Test
@@ -167,6 +172,16 @@ void testWatcherSemanticsWhenRegisteredAtMethodLevel() {
167172
assertThat(TrackingTestWatcher.results.get("testDisabled")).containsExactly("test", "repeatedTest");
168173
}
169174

175+
@Test
176+
void testWatcherCanRetrieveDataFromTheExtensionContextStore() {
177+
Class<?> testClass = DataRetrievingTestWatcherTestCase.class;
178+
EngineExecutionResults results = executeTestsForClass(testClass);
179+
180+
results.testEvents().assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
181+
182+
assertThat(DataRetrievingTestWatcher.results).containsExactly(entry("key", "enigma"));
183+
}
184+
170185
private void assertCommonStatistics(EngineExecutionResults results) {
171186
results.containerEvents().assertStatistics(stats -> stats.started(3).succeeded(3).failed(0));
172187
results.testEvents().assertStatistics(stats -> stats.skipped(2).started(6).succeeded(2).aborted(2).failed(2));
@@ -414,4 +429,40 @@ public void testFailed(ExtensionContext context, Throwable cause) {
414429

415430
}
416431

432+
/**
433+
* {@link TestWatcher} that retrieves data from the {@link ExtensionContext.Store}.
434+
* @see <a href="https://github.com/junit-team/junit5/issues/3944">#3944</a>
435+
*/
436+
static class DataRetrievingTestWatcher implements BeforeTestExecutionCallback, TestWatcher {
437+
438+
private static final Namespace NAMESPACE = Namespace.create(DataRetrievingTestWatcher.class);
439+
440+
private static final String KEY = "key";
441+
442+
private static final Map<String, String> results = new HashMap<>();
443+
444+
@Override
445+
public void beforeTestExecution(ExtensionContext context) throws Exception {
446+
getStore(context).put(KEY, "enigma");
447+
}
448+
449+
@Override
450+
public void testSuccessful(ExtensionContext context) {
451+
results.put(KEY, getStore(context).get(KEY, String.class));
452+
}
453+
454+
private static Store getStore(ExtensionContext context) {
455+
return context.getStore(NAMESPACE);
456+
}
457+
}
458+
459+
@ExtendWith(DataRetrievingTestWatcher.class)
460+
static class DataRetrievingTestWatcherTestCase {
461+
462+
@Test
463+
public void test() {
464+
//no-op
465+
}
466+
}
467+
417468
}

platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java

+17-10
Original file line numberDiff line numberDiff line change
@@ -519,28 +519,35 @@ void closeIsIdempotent() throws Throwable {
519519
verifyNoMoreInteractions(closeAction);
520520
}
521521

522+
/**
523+
* @see <a href="https://github.com/junit-team/junit5/issues/3944">#3944</a>
524+
*/
522525
@Test
523-
void rejectsModificationAfterClose() {
526+
void acceptsQueryAfterClose() {
527+
store.put(namespace, key, value);
524528
store.close();
525529
assertClosed();
526530

527-
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.put(namespace, "key1", "value1"));
528-
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.remove(namespace, "key1"));
529-
assertThrows(NamespacedHierarchicalStoreException.class,
530-
() -> store.remove(namespace, "key1", Number.class));
531+
assertThat(store.get(namespace, key)).isEqualTo(value);
532+
assertThat(store.get(namespace, key, String.class)).isEqualTo(value);
533+
assertThat(store.getOrComputeIfAbsent(namespace, key, k -> "new")).isEqualTo(value);
534+
assertThat(store.getOrComputeIfAbsent(namespace, key, k -> "new", String.class)).isEqualTo(value);
531535
}
532536

533537
@Test
534-
void rejectsQueryAfterClose() {
538+
void rejectsModificationAfterClose() {
535539
store.close();
536540
assertClosed();
537541

538-
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.get(namespace, "key1"));
539-
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.get(namespace, "key1", Integer.class));
542+
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.put(namespace, key, value));
543+
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.remove(namespace, key));
544+
assertThrows(NamespacedHierarchicalStoreException.class, () -> store.remove(namespace, key, int.class));
545+
546+
// Since key does not exist, an invocation of getOrComputeIfAbsent(...) will attempt to compute a new value.
540547
assertThrows(NamespacedHierarchicalStoreException.class,
541-
() -> store.getOrComputeIfAbsent(namespace, "key1", k -> "value"));
548+
() -> store.getOrComputeIfAbsent(namespace, key, k -> "new"));
542549
assertThrows(NamespacedHierarchicalStoreException.class,
543-
() -> store.getOrComputeIfAbsent(namespace, "key1", k -> 1337, Integer.class));
550+
() -> store.getOrComputeIfAbsent(namespace, key, k -> "new", String.class));
544551
}
545552

546553
private void assertNotClosed() {

0 commit comments

Comments
 (0)