|
25 | 25 | import static org.mockito.Mockito.verifyNoInteractions; |
26 | 26 | import static org.mockito.Mockito.verifyNoMoreInteractions; |
27 | 27 |
|
| 28 | +import java.io.Serial; |
28 | 29 | import java.util.List; |
29 | 30 | import java.util.concurrent.atomic.AtomicInteger; |
30 | 31 | import java.util.function.Function; |
@@ -82,6 +83,13 @@ void valueIsComputedIfAbsent() { |
82 | 83 | assertEquals(value, store.get(namespace, key)); |
83 | 84 | } |
84 | 85 |
|
| 86 | + @Test |
| 87 | + void valueIsComputedIfNull() { |
| 88 | + assertNull(store.put(namespace, key, null)); |
| 89 | + assertEquals(value, store.computeIfAbsent(namespace, key, __ -> value)); |
| 90 | + assertEquals(value, store.get(namespace, key)); |
| 91 | + } |
| 92 | + |
85 | 93 | @SuppressWarnings("deprecation") |
86 | 94 | @Test |
87 | 95 | void valueIsNotComputedIfPresentLocally() { |
@@ -316,22 +324,42 @@ void computeIfAbsentWithTypeSafetyAndPrimitiveValueType() { |
316 | 324 | @SuppressWarnings("deprecation") |
317 | 325 | @Test |
318 | 326 | void getOrComputeIfAbsentWithExceptionThrowingCreatorFunction() { |
319 | | - var e = assertThrows(RuntimeException.class, () -> store.getOrComputeIfAbsent(namespace, key, __ -> { |
320 | | - throw new RuntimeException("boom"); |
| 327 | + var e = assertThrows(ComputeException.class, () -> store.getOrComputeIfAbsent(namespace, key, __ -> { |
| 328 | + throw new ComputeException("boom"); |
321 | 329 | })); |
322 | | - assertSame(e, assertThrows(RuntimeException.class, () -> store.get(namespace, key))); |
323 | | - assertSame(e, assertThrows(RuntimeException.class, () -> store.remove(namespace, key))); |
| 330 | + assertSame(e, assertThrows(ComputeException.class, () -> store.get(namespace, key))); |
| 331 | + assertSame(e, assertThrows(ComputeException.class, () -> store.remove(namespace, key))); |
324 | 332 | } |
325 | 333 |
|
326 | 334 | @Test |
327 | 335 | void computeIfAbsentWithExceptionThrowingCreatorFunction() { |
328 | | - assertThrows(RuntimeException.class, () -> store.computeIfAbsent(namespace, key, __ -> { |
329 | | - throw new RuntimeException("boom"); |
| 336 | + assertThrows(ComputeException.class, () -> store.computeIfAbsent(namespace, key, __ -> { |
| 337 | + throw new ComputeException("boom"); |
330 | 338 | })); |
331 | 339 | assertNull(store.get(namespace, key)); |
332 | 340 | assertNull(store.remove(namespace, key)); |
333 | 341 | } |
334 | 342 |
|
| 343 | + @SuppressWarnings("deprecation") |
| 344 | + @Test |
| 345 | + void getOrComputeIfAbsentDoesNotSeeComputeIfAbsentWithExceptionThrowingCreatorFunction() { |
| 346 | + assertThrows(ComputeException.class, () -> store.computeIfAbsent(namespace, key, __ -> { |
| 347 | + throw new ComputeException("boom"); |
| 348 | + })); |
| 349 | + assertNull(store.get(namespace, key)); |
| 350 | + assertEquals(value, store.getOrComputeIfAbsent(namespace, key, __ -> value)); |
| 351 | + } |
| 352 | + |
| 353 | + @SuppressWarnings("deprecation") |
| 354 | + @Test |
| 355 | + void computeIfAbsentSeesGetOrComputeIfAbsentWithExceptionThrowingCreatorFunction() { |
| 356 | + assertThrows(ComputeException.class, () -> store.getOrComputeIfAbsent(namespace, key, __ -> { |
| 357 | + throw new ComputeException("boom"); |
| 358 | + })); |
| 359 | + assertThrows(ComputeException.class, () -> store.get(namespace, key)); |
| 360 | + assertThrows(ComputeException.class, () -> store.computeIfAbsent(namespace, key, __ -> value)); |
| 361 | + } |
| 362 | + |
335 | 363 | @Test |
336 | 364 | void removeWithTypeSafetyAndInvalidRequiredTypeThrowsException() { |
337 | 365 | Integer key = 42; |
@@ -416,6 +444,35 @@ void simulateRaceConditionInComputeIfAbsent() throws Exception { |
416 | 444 | assertEquals(1, counter.get()); |
417 | 445 | assertThat(values).hasSize(threads).containsOnly(1); |
418 | 446 | } |
| 447 | + |
| 448 | + @SuppressWarnings("deprecation") |
| 449 | + @Test |
| 450 | + void updateRecursivelyGetOrComputeIfAbsent() { |
| 451 | + try (var localStore = new NamespacedHierarchicalStore<>(null)) { |
| 452 | + var value = localStore.getOrComputeIfAbsent(namespace, new CollidingKey("a"), // |
| 453 | + a -> requireNonNull(localStore.getOrComputeIfAbsent(namespace, new CollidingKey("b"), // |
| 454 | + b -> "enigma"))); |
| 455 | + assertEquals("enigma", value); |
| 456 | + } |
| 457 | + } |
| 458 | + |
| 459 | + @Test |
| 460 | + void updateRecursivelyComputeIfAbsent() { |
| 461 | + try (var localStore = new NamespacedHierarchicalStore<>(null)) { |
| 462 | + var value = localStore.computeIfAbsent(namespace, new CollidingKey("a"), // |
| 463 | + a -> localStore.computeIfAbsent(namespace, new CollidingKey("b"), // |
| 464 | + b -> "enigma")); |
| 465 | + assertEquals("enigma", value); |
| 466 | + } |
| 467 | + } |
| 468 | + |
| 469 | + private record CollidingKey(String value) { |
| 470 | + |
| 471 | + @Override |
| 472 | + public int hashCode() { |
| 473 | + return 42; |
| 474 | + } |
| 475 | + } |
419 | 476 | } |
420 | 477 |
|
421 | 478 | @Nested |
@@ -522,19 +579,19 @@ void doesNotCallCloseActionForNullValues() { |
522 | 579 | @Test |
523 | 580 | void doesNotCallCloseActionForValuesThatThrowExceptionsDuringCleanup() throws Throwable { |
524 | 581 | store.put(namespace, "key1", "value1"); |
525 | | - assertThrows(RuntimeException.class, () -> store.computeIfAbsent(namespace, "key2", __ -> { |
526 | | - throw new RuntimeException("boom"); |
| 582 | + assertThrows(ComputeException.class, () -> store.computeIfAbsent(namespace, "key2", __ -> { |
| 583 | + throw new ComputeException("boom"); |
527 | 584 | })); |
528 | | - assertThrows(RuntimeException.class, () -> store.getOrComputeIfAbsent(namespace, "key2", __ -> { |
529 | | - throw new RuntimeException("boom"); |
| 585 | + assertThrows(ComputeException.class, () -> store.getOrComputeIfAbsent(namespace, "key3", __ -> { |
| 586 | + throw new ComputeException("boom"); |
530 | 587 | })); |
531 | | - store.put(namespace, "key3", "value3"); |
| 588 | + store.put(namespace, "key4", "value4"); |
532 | 589 |
|
533 | 590 | store.close(); |
534 | 591 | assertClosed(); |
535 | 592 |
|
536 | 593 | var inOrder = inOrder(closeAction); |
537 | | - inOrder.verify(closeAction).close(namespace, "key3", "value3"); |
| 594 | + inOrder.verify(closeAction).close(namespace, "key4", "value4"); |
538 | 595 | inOrder.verify(closeAction).close(namespace, "key1", "value1"); |
539 | 596 | inOrder.verifyNoMoreInteractions(); |
540 | 597 | } |
@@ -672,4 +729,17 @@ public String toString() { |
672 | 729 | } |
673 | 730 | }; |
674 | 731 | } |
| 732 | + |
| 733 | + /** |
| 734 | + * To avoid confusion with other Runtime exceptions that can be thrown. |
| 735 | + */ |
| 736 | + private static final class ComputeException extends RuntimeException { |
| 737 | + |
| 738 | + @Serial |
| 739 | + private static final long serialVersionUID = 1L; |
| 740 | + |
| 741 | + ComputeException(String msg) { |
| 742 | + super(msg); |
| 743 | + } |
| 744 | + } |
675 | 745 | } |
0 commit comments