From 8d51164b9d697f307640d131b9f281115e6f64df Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Mon, 14 Mar 2022 19:15:13 -0700 Subject: [PATCH] resync jsr166 tck tests for more scenarios --- .../caffeine/cache/BoundedLocalCache.java | 2 +- .../caffeine/cache/SnapshotEntry.java | 16 +- .../benmanes/caffeine/cache/AsMapTest.java | 5 +- .../caffeine/jsr166/Collection8Test.java | 1045 +++++++++ .../jsr166/CollectionImplementation.java | 21 + .../caffeine/jsr166/CollectionTest.java | 36 + .../jsr166/ConcurrentHashMap8Test.java | 877 +++++-- .../jsr166/ConcurrentHashMapTest.java | 483 ++-- .../github/benmanes/caffeine/jsr166/Item.java | 64 + .../caffeine/jsr166/JSR166TestCase.java | 2018 +++++++++++++---- .../benmanes/caffeine/jsr166/KeySetTest.java | 59 + .../caffeine/jsr166/MapImplementation.java | 30 + .../benmanes/caffeine/jsr166/MapTest.java | 299 +++ 13 files changed, 4211 insertions(+), 744 deletions(-) create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Collection8Test.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionImplementation.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionTest.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Item.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/KeySetTest.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapImplementation.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapTest.java diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 62e3d64e05..bfaa201004 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -2800,7 +2800,7 @@ public Set> entrySet() { *

* The consistency property between invocations requires that the results are the same if * there are no modifications to the information used. Therefore, usages should expect that this - * operation may return misleading results if either the map or the data held by them is modified + * operation may return misleading results if either map or the data held by them is modified * during the execution of this method. This characteristic allows for comparing the map sizes and * assuming stable mappings, as done by {@link AbstractMap}-based maps. *

diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SnapshotEntry.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SnapshotEntry.java index bc7cff147a..b05e60f619 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SnapshotEntry.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SnapshotEntry.java @@ -1,15 +1,17 @@ /* * Copyright 2022 Ben Manes. All Rights Reserved. * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.github.benmanes.caffeine.cache; diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java index b6b789e07d..0c077caf19 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java @@ -28,6 +28,7 @@ import static com.google.common.collect.Maps.immutableEntry; import static com.google.common.truth.Truth.assertThat; import static java.util.Map.entry; +import static java.util.stream.Collectors.toMap; import java.util.AbstractMap; import java.util.ArrayList; @@ -62,7 +63,6 @@ import com.github.benmanes.caffeine.testing.ConcurrentTestHarness; import com.github.benmanes.caffeine.testing.Int; import com.google.common.base.Splitter; -import com.google.common.collect.Maps; import com.google.common.testing.SerializableTester; /** @@ -1559,7 +1559,8 @@ public void equals(Map map, CacheContext context) { assertThat(context.absent().equals(map)).isFalse(); if (!map.isEmpty()) { - var other = Maps.asMap(map.keySet(), CompletableFuture::completedFuture); + var other = map.entrySet().stream().collect(toMap( + entry -> entry.getKey(), entry -> entry.getValue().negate())); assertThat(map.equals(other)).isFalse(); assertThat(other.equals(map)).isFalse(); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Collection8Test.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Collection8Test.java new file mode 100644 index 0000000000..7945cdcb18 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Collection8Test.java @@ -0,0 +1,1045 @@ +/* + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Set; +import java.util.Spliterator; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import junit.framework.Test; + +/** + * Contains tests applicable to all jdk8+ Collection implementations. + * An extension of CollectionTest. + */ +@SuppressWarnings({"rawtypes", "try", "unchecked", "CatchAndPrintStackTrace", "MissingDefault", + "MissingFail", "ReturnValueIgnored", "UnnecessaryParentheses"}) +public class Collection8Test extends JSR166TestCase { + final CollectionImplementation impl; + + /** Tests are parameterized by a Collection implementation. */ + Collection8Test(CollectionImplementation impl, String methodName) { + super(methodName); + this.impl = impl; + } + + public static Test testSuite(CollectionImplementation impl) { + return parameterizedTestSuite(Collection8Test.class, + CollectionImplementation.class, + impl); + } + + Object bomb() { + return new Object() { + @Override public boolean equals(Object x) { throw new AssertionError(); } + @Override public int hashCode() { throw new AssertionError(); } + @Override public String toString() { throw new AssertionError(); } + }; + } + + /** Checks properties of empty collections. */ + public void testEmptyMeansEmpty() throws Throwable { + Collection c = impl.emptyCollection(); + emptyMeansEmpty(c); + + if (c instanceof java.io.Serializable) { + try { + emptyMeansEmpty(serialClonePossiblyFailing(c)); + } catch (java.io.NotSerializableException ex) { + // excusable when we have a serializable wrapper around + // a non-serializable collection, as can happen with: + // Vector.subList() => wrapped AbstractList$RandomAccessSubList + if (testImplementationDetails + && (! c.getClass().getName().matches( + "java.util.Collections.*"))) { + throw ex; + } + } + } + + Collection clone = cloneableClone(c); + if (clone != null) { + emptyMeansEmpty(clone); + } + } + + void emptyMeansEmpty(Collection c) throws InterruptedException { + assertTrue(c.isEmpty()); + mustEqual(0, c.size()); + mustEqual("[]", c.toString()); + if (c instanceof List) { + List x = (List) c; + mustEqual(1, x.hashCode()); + mustEqual(x, Collections.emptyList()); + mustEqual(Collections.emptyList(), x); + mustEqual(-1, x.indexOf(impl.makeElement(86))); + mustEqual(-1, x.lastIndexOf(impl.makeElement(99))); + assertThrows( + IndexOutOfBoundsException.class, + () -> x.get(0), + () -> x.set(0, impl.makeElement(42))); + } + else if (c instanceof Set) { + mustEqual(0, c.hashCode()); + mustEqual(c, Collections.emptySet()); + mustEqual(Collections.emptySet(), c); + } + { + Object[] a = c.toArray(); + mustEqual(0, a.length); + assertSame(Object[].class, a.getClass()); + } + { + Object[] a = new Object[0]; + assertSame(a, c.toArray(a)); + } + { + Item[] a = new Item[0]; + assertSame(a, c.toArray(a)); + } + { + Item[] a = { one, two, three}; + assertSame(a, c.toArray(a)); + assertNull(a[0]); + mustEqual(2, a[1]); + mustEqual(3, a[2]); + } + assertIteratorExhausted(c.iterator()); + Consumer alwaysThrows = e -> { throw new AssertionError(); }; + c.forEach(alwaysThrows); + c.iterator().forEachRemaining(alwaysThrows); + c.spliterator().forEachRemaining(alwaysThrows); + assertFalse(c.spliterator().tryAdvance(alwaysThrows)); + if (c.spliterator().hasCharacteristics(Spliterator.SIZED)) { + mustEqual(0, c.spliterator().estimateSize()); + } +// assertFalse(c.contains(bomb())); +// assertFalse(c.remove(bomb())); + if (c instanceof Queue) { + Queue q = (Queue) c; + assertNull(q.peek()); + assertNull(q.poll()); + } + if (c instanceof Deque) { + Deque d = (Deque) c; + assertNull(d.peekFirst()); + assertNull(d.peekLast()); + assertNull(d.pollFirst()); + assertNull(d.pollLast()); + assertIteratorExhausted(d.descendingIterator()); + d.descendingIterator().forEachRemaining(alwaysThrows); + assertFalse(d.removeFirstOccurrence(bomb())); + assertFalse(d.removeLastOccurrence(bomb())); + } + if (c instanceof BlockingQueue) { + BlockingQueue q = (BlockingQueue) c; + assertNull(q.poll(randomExpiredTimeout(), randomTimeUnit())); + } + if (c instanceof BlockingDeque) { + BlockingDeque q = (BlockingDeque) c; + assertNull(q.pollFirst(randomExpiredTimeout(), randomTimeUnit())); + assertNull(q.pollLast(randomExpiredTimeout(), randomTimeUnit())); + } + } + + public void testNullPointerExceptions() throws InterruptedException { + Collection c = impl.emptyCollection(); + Collection nullCollection = null; + assertThrows( + NullPointerException.class, + () -> c.addAll(nullCollection), + () -> c.containsAll(nullCollection), + () -> c.retainAll(nullCollection), + () -> c.removeAll(nullCollection), + () -> c.removeIf(null), + () -> c.forEach(null), + () -> c.iterator().forEachRemaining(null), + () -> c.spliterator().forEachRemaining(null), + () -> c.spliterator().tryAdvance(null), + () -> c.toArray((Object[])null)); + + if (!impl.permitsNulls()) { + assertThrows( + NullPointerException.class, + () -> c.add(null)); + } + if (!impl.permitsNulls() && c instanceof Queue) { + Queue q = (Queue) c; + assertThrows( + NullPointerException.class, + () -> q.offer(null)); + } + if (!impl.permitsNulls() && c instanceof Deque) { + Deque d = (Deque) c; + assertThrows( + NullPointerException.class, + () -> d.addFirst(null), + () -> d.addLast(null), + () -> d.offerFirst(null), + () -> d.offerLast(null), + () -> d.push(null), + () -> d.descendingIterator().forEachRemaining(null)); + } + if (c instanceof BlockingQueue) { + BlockingQueue q = (BlockingQueue) c; + assertThrows( + NullPointerException.class, + () -> q.offer(null, 1L, HOURS), + () -> q.put(null)); + } + if (c instanceof BlockingDeque) { + BlockingDeque q = (BlockingDeque) c; + assertThrows( + NullPointerException.class, + () -> q.offerFirst(null, 1L, HOURS), + () -> q.offerLast(null, 1L, HOURS), + () -> q.putFirst(null), + () -> q.putLast(null)); + } + } + + public void testNoSuchElementExceptions() { + Collection c = impl.emptyCollection(); + assertThrows( + NoSuchElementException.class, + () -> c.iterator().next()); + + if (c instanceof Queue) { + Queue q = (Queue) c; + assertThrows( + NoSuchElementException.class, + () -> q.element(), + () -> q.remove()); + } + if (c instanceof Deque) { + Deque d = (Deque) c; + assertThrows( + NoSuchElementException.class, + () -> d.getFirst(), + () -> d.getLast(), + () -> d.removeFirst(), + () -> d.removeLast(), + () -> d.pop(), + () -> d.descendingIterator().next()); + } + if (c instanceof List) { + List x = (List) c; + assertThrows( + NoSuchElementException.class, + () -> x.iterator().next(), + () -> x.listIterator().next(), + () -> x.listIterator(0).next(), + () -> x.listIterator().previous(), + () -> x.listIterator(0).previous()); + } + } + + public void testRemoveIf() { + Collection c = impl.emptyCollection(); + boolean ordered = + c.spliterator().hasCharacteristics(Spliterator.ORDERED); + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + int n = rnd.nextInt(6); + for (int i = 0; i < n; i++) { + c.add(impl.makeElement(i)); + } + AtomicReference threwAt = new AtomicReference(null); + List orig = rnd.nextBoolean() + ? new ArrayList(c) + : Arrays.asList(c.toArray()); + + // Merely creating an iterator can change ArrayBlockingQueue behavior + Iterator it = rnd.nextBoolean() ? c.iterator() : null; + + ArrayList survivors = new ArrayList(); + ArrayList accepts = new ArrayList(); + ArrayList rejects = new ArrayList(); + + Predicate randomPredicate = e -> { + assertNull(threwAt.get()); + switch (rnd.nextInt(3)) { + case 0: accepts.add(e); return true; + case 1: rejects.add(e); return false; + case 2: threwAt.set(e); throw new ArithmeticException(); + default: throw new AssertionError(); + } + }; + try { + try { + boolean modified = c.removeIf(randomPredicate); + assertNull(threwAt.get()); + mustEqual(modified, accepts.size() > 0); + mustEqual(modified, rejects.size() != n); + mustEqual(accepts.size() + rejects.size(), n); + if (ordered) { + mustEqual(rejects, + Arrays.asList(c.toArray())); + } else { + mustEqual(new HashSet(rejects), + new HashSet(Arrays.asList(c.toArray()))); + } + } catch (ArithmeticException ok) { + assertNotNull(threwAt.get()); + assertTrue(c.contains(threwAt.get())); + } + if (it != null && impl.isConcurrent()) { + // check for weakly consistent iterator + while (it.hasNext()) { + assertTrue(orig.contains(it.next())); + } + } + switch (rnd.nextInt(4)) { + case 0: survivors.addAll(c); break; + case 1: survivors.addAll(Arrays.asList(c.toArray())); break; + case 2: c.forEach(survivors::add); break; + case 3: for (Object e : c) { + survivors.add(e); + } break; + } + assertTrue(orig.containsAll(accepts)); + assertTrue(orig.containsAll(rejects)); + assertTrue(orig.containsAll(survivors)); + assertTrue(orig.containsAll(c)); + assertTrue(c.containsAll(rejects)); + assertTrue(c.containsAll(survivors)); + assertTrue(survivors.containsAll(rejects)); + if (threwAt.get() == null) { + mustEqual(n - accepts.size(), c.size()); + for (Object x : accepts) { + assertFalse(c.contains(x)); + } + } else { + // Two acceptable behaviors: entire removeIf call is one + // transaction, or each element processed is one transaction. + assertTrue(n == c.size() || n == c.size() + accepts.size()); + int k = 0; + for (Object x : accepts) { + if (c.contains(x)) { + k++; + } + } + assertTrue(k == accepts.size() || k == 0); + } + } catch (Throwable ex) { + System.err.println(impl.klazz()); + // c is at risk of corruption if we got here, so be lenient + try { System.err.printf("c=%s%n", c); } + catch (Throwable t) { t.printStackTrace(); } + System.err.printf("n=%d%n", n); + System.err.printf("orig=%s%n", orig); + System.err.printf("accepts=%s%n", accepts); + System.err.printf("rejects=%s%n", rejects); + System.err.printf("survivors=%s%n", survivors); + System.err.printf("threwAt=%s%n", threwAt.get()); + throw ex; + } + } + + /** + * All elements removed in the middle of CONCURRENT traversal. + */ + public void testElementRemovalDuringTraversal() { + Collection c = impl.emptyCollection(); + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + int n = rnd.nextInt(6); + ArrayList copy = new ArrayList(); + for (int i = 0; i < n; i++) { + Object x = impl.makeElement(i); + copy.add(x); + c.add(x); + } + ArrayList iterated = new ArrayList(); + ArrayList spliterated = new ArrayList(); + Spliterator s = c.spliterator(); + Iterator it = c.iterator(); + for (int i = rnd.nextInt(n + 1); --i >= 0; ) { + assertTrue(s.tryAdvance(spliterated::add)); + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + iterated.add(it.next()); + } + Consumer alwaysThrows = e -> { throw new AssertionError(); }; + if (s.hasCharacteristics(Spliterator.CONCURRENT)) { + c.clear(); // TODO: many more removal methods + if (testImplementationDetails + && !(c instanceof java.util.concurrent.ArrayBlockingQueue)) { + if (rnd.nextBoolean()) { + assertFalse(s.tryAdvance(alwaysThrows)); + } else { + s.forEachRemaining(alwaysThrows); + } + } + if (it.hasNext()) { + iterated.add(it.next()); + } + if (rnd.nextBoolean()) { + assertIteratorExhausted(it); + } + } + assertTrue(copy.containsAll(iterated)); + assertTrue(copy.containsAll(spliterated)); + } + + /** + * Some elements randomly disappear in the middle of traversal. + */ + public void testRandomElementRemovalDuringTraversal() { + Collection c = impl.emptyCollection(); + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + int n = rnd.nextInt(6); + ArrayList copy = new ArrayList(); + for (int i = 0; i < n; i++) { + Object x = impl.makeElement(i); + copy.add(x); + c.add(x); + } + ArrayList iterated = new ArrayList(); + ArrayList spliterated = new ArrayList(); + ArrayList removed = new ArrayList(); + Spliterator s = c.spliterator(); + Iterator it = c.iterator(); + if (! (s.hasCharacteristics(Spliterator.CONCURRENT) || + s.hasCharacteristics(Spliterator.IMMUTABLE))) { + return; + } + for (int i = rnd.nextInt(n + 1); --i >= 0; ) { + assertTrue(s.tryAdvance(e -> {})); + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + it.next(); + } + // TODO: many more removal methods + if (rnd.nextBoolean()) { + for (Iterator z = c.iterator(); z.hasNext(); ) { + Object e = z.next(); + if (rnd.nextBoolean()) { + try { + z.remove(); + } catch (UnsupportedOperationException ok) { return; } + removed.add(e); + } + } + } else { + Predicate randomlyRemove = e -> { + if (rnd.nextBoolean()) { removed.add(e); return true; } else { + return false; + } + }; + c.removeIf(randomlyRemove); + } + s.forEachRemaining(spliterated::add); + while (it.hasNext()) { + iterated.add(it.next()); + } + assertTrue(copy.containsAll(iterated)); + assertTrue(copy.containsAll(spliterated)); + assertTrue(copy.containsAll(removed)); + if (s.hasCharacteristics(Spliterator.CONCURRENT)) { + ArrayList iteratedAndRemoved = new ArrayList(iterated); + ArrayList spliteratedAndRemoved = new ArrayList(spliterated); + iteratedAndRemoved.retainAll(removed); + spliteratedAndRemoved.retainAll(removed); + assertTrue(iteratedAndRemoved.size() <= 1); + assertTrue(spliteratedAndRemoved.size() <= 1); + if (testImplementationDetails + && !(c instanceof java.util.concurrent.ArrayBlockingQueue)) { + assertTrue(spliteratedAndRemoved.isEmpty()); + } + } + } + + /** + * Various ways of traversing a collection yield same elements + */ + public void testTraversalEquivalence() { + Collection c = impl.emptyCollection(); + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + int n = rnd.nextInt(6); + for (int i = 0; i < n; i++) { + c.add(impl.makeElement(i)); + } + ArrayList iterated = new ArrayList(); + ArrayList iteratedForEachRemaining = new ArrayList(); + ArrayList tryAdvanced = new ArrayList(); + ArrayList spliterated = new ArrayList(); + ArrayList splitonced = new ArrayList(); + ArrayList forEached = new ArrayList(); + ArrayList streamForEached = new ArrayList(); + ConcurrentLinkedQueue parallelStreamForEached = new ConcurrentLinkedQueue(); + ArrayList removeIfed = new ArrayList(); + for (Object x : c) { + iterated.add(x); + } + c.iterator().forEachRemaining(iteratedForEachRemaining::add); + for (Spliterator s = c.spliterator(); + s.tryAdvance(tryAdvanced::add); ) {} + c.spliterator().forEachRemaining(spliterated::add); + { // trySplit returns "strict prefix" + Spliterator s1 = c.spliterator(), s2 = s1.trySplit(); + if (s2 != null) { + s2.forEachRemaining(splitonced::add); + } + s1.forEachRemaining(splitonced::add); + } + c.forEach(forEached::add); + c.stream().forEach(streamForEached::add); + c.parallelStream().forEach(parallelStreamForEached::add); + c.removeIf(e -> { removeIfed.add(e); return false; }); + boolean ordered = + c.spliterator().hasCharacteristics(Spliterator.ORDERED); + if (c instanceof List || c instanceof Deque) { + assertTrue(ordered); + } + HashSet cset = new HashSet(c); + mustEqual(cset, new HashSet(parallelStreamForEached)); + if (ordered) { + mustEqual(iterated, iteratedForEachRemaining); + mustEqual(iterated, tryAdvanced); + mustEqual(iterated, spliterated); + mustEqual(iterated, splitonced); + mustEqual(iterated, forEached); + mustEqual(iterated, streamForEached); + mustEqual(iterated, removeIfed); + } else { + mustEqual(cset, new HashSet(iterated)); + mustEqual(cset, new HashSet(iteratedForEachRemaining)); + mustEqual(cset, new HashSet(tryAdvanced)); + mustEqual(cset, new HashSet(spliterated)); + mustEqual(cset, new HashSet(splitonced)); + mustEqual(cset, new HashSet(forEached)); + mustEqual(cset, new HashSet(streamForEached)); + mustEqual(cset, new HashSet(removeIfed)); + } + if (c instanceof Deque) { + Deque d = (Deque) c; + ArrayList descending = new ArrayList(); + ArrayList descendingForEachRemaining = new ArrayList(); + for (Iterator it = d.descendingIterator(); it.hasNext(); ) { + descending.add(it.next()); + } + d.descendingIterator().forEachRemaining( + e -> descendingForEachRemaining.add(e)); + Collections.reverse(descending); + Collections.reverse(descendingForEachRemaining); + mustEqual(iterated, descending); + mustEqual(iterated, descendingForEachRemaining); + } + } + + /** + * Iterator.forEachRemaining has same behavior as Iterator's + * default implementation. + */ + public void testForEachRemainingConsistentWithDefaultImplementation() { + Collection c = impl.emptyCollection(); + if (!testImplementationDetails + || c.getClass() == java.util.LinkedList.class) { + return; + } + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + int n = 1 + rnd.nextInt(3); + for (int i = 0; i < n; i++) { + c.add(impl.makeElement(i)); + } + ArrayList iterated = new ArrayList(); + ArrayList iteratedForEachRemaining = new ArrayList(); + Iterator it1 = c.iterator(); + Iterator it2 = c.iterator(); + assertTrue(it1.hasNext()); + assertTrue(it2.hasNext()); + c.clear(); + Object r1, r2; + try { + while (it1.hasNext()) { + iterated.add(it1.next()); + } + r1 = iterated; + } catch (ConcurrentModificationException ex) { + r1 = ConcurrentModificationException.class; + assertFalse(impl.isConcurrent()); + } + try { + it2.forEachRemaining(iteratedForEachRemaining::add); + r2 = iteratedForEachRemaining; + } catch (ConcurrentModificationException ex) { + r2 = ConcurrentModificationException.class; + assertFalse(impl.isConcurrent()); + } + mustEqual(r1, r2); + } + + /** + * Calling Iterator#remove() after Iterator#forEachRemaining + * should (maybe) remove last element + */ + public void testRemoveAfterForEachRemaining() { + Collection c = impl.emptyCollection(); + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + ArrayList copy = new ArrayList(); + boolean ordered = c.spliterator().hasCharacteristics(Spliterator.ORDERED); + testCollection: { + int n = 3 + rnd.nextInt(2); + for (int i = 0; i < n; i++) { + Object x = impl.makeElement(i); + c.add(x); + copy.add(x); + } + Iterator it = c.iterator(); + if (ordered) { + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + mustEqual(impl.makeElement(0), it.next()); + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + mustEqual(impl.makeElement(1), it.next()); + } else { + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + assertTrue(copy.contains(it.next())); + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + assertTrue(copy.contains(it.next())); + } + if (rnd.nextBoolean()) { + assertTrue(it.hasNext()); + } + it.forEachRemaining( + e -> { + assertTrue(c.contains(e)); + assertTrue(copy.contains(e));}); + if (testImplementationDetails) { + if (c instanceof java.util.concurrent.ArrayBlockingQueue) { + assertIteratorExhausted(it); + } else { + try { it.remove(); } + catch (UnsupportedOperationException ok) { + break testCollection; + } + mustEqual(n - 1, c.size()); + if (ordered) { + for (int i = 0; i < n - 1; i++) { + assertTrue(c.contains(impl.makeElement(i))); + } + assertFalse(c.contains(impl.makeElement(n - 1))); + } + } + } + } + if (c instanceof Deque) { + Deque d = (Deque) impl.emptyCollection(); + assertTrue(ordered); + int n = 3 + rnd.nextInt(2); + for (int i = 0; i < n; i++) { + d.add(impl.makeElement(i)); + } + Iterator it = d.descendingIterator(); + assertTrue(it.hasNext()); + mustEqual(impl.makeElement(n - 1), it.next()); + assertTrue(it.hasNext()); + mustEqual(impl.makeElement(n - 2), it.next()); + it.forEachRemaining(e -> assertTrue(c.contains(e))); + if (testImplementationDetails) { + it.remove(); + mustEqual(n - 1, d.size()); + for (int i = 1; i < n; i++) { + assertTrue(d.contains(impl.makeElement(i))); + } + assertFalse(d.contains(impl.makeElement(0))); + } + } + } + + /** + * stream().forEach returns elements in the collection + */ + public void testStreamForEach() throws Throwable { + final Collection c = impl.emptyCollection(); + final Object x = impl.makeElement(1); + final Object y = impl.makeElement(2); + final ArrayList found = new ArrayList(); + Consumer spy = o -> found.add(o); + c.stream().forEach(spy); + assertTrue(found.isEmpty()); + + assertTrue(c.add(x)); + c.stream().forEach(spy); + mustEqual(Collections.singletonList(x), found); + found.clear(); + + assertTrue(c.add(y)); + c.stream().forEach(spy); + mustEqual(2, found.size()); + assertTrue(found.contains(x)); + assertTrue(found.contains(y)); + found.clear(); + + c.clear(); + c.stream().forEach(spy); + assertTrue(found.isEmpty()); + } + + public void testStreamForEachConcurrentStressTest() throws Throwable { + if (!impl.isConcurrent()) { + return; + } + final Collection c = impl.emptyCollection(); + final long testDurationMillis = timeoutMillis(); + final AtomicBoolean done = new AtomicBoolean(false); + final Object elt = impl.makeElement(1); + final Future f1, f2; + final ExecutorService pool = Executors.newCachedThreadPool(); + try (PoolCleaner cleaner = cleaner(pool, done)) { + final CountDownLatch threadsStarted = new CountDownLatch(2); + Runnable checkElt = () -> { + threadsStarted.countDown(); + while (!done.get()) { + c.stream().forEach(x -> assertSame(x, elt)); + } }; + Runnable addRemove = () -> { + threadsStarted.countDown(); + while (!done.get()) { + assertTrue(c.add(elt)); + assertTrue(c.remove(elt)); + }}; + f1 = pool.submit(checkElt); + f2 = pool.submit(addRemove); + Thread.sleep(testDurationMillis); + } + assertNull(f1.get(0L, MILLISECONDS)); + assertNull(f2.get(0L, MILLISECONDS)); + } + + /** + * collection.forEach returns elements in the collection + */ + public void testForEach() throws Throwable { + final Collection c = impl.emptyCollection(); + final Object x = impl.makeElement(1); + final Object y = impl.makeElement(2); + final ArrayList found = new ArrayList(); + Consumer spy = o -> found.add(o); + c.forEach(spy); + assertTrue(found.isEmpty()); + + assertTrue(c.add(x)); + c.forEach(spy); + mustEqual(Collections.singletonList(x), found); + found.clear(); + + assertTrue(c.add(y)); + c.forEach(spy); + mustEqual(2, found.size()); + assertTrue(found.contains(x)); + assertTrue(found.contains(y)); + found.clear(); + + c.clear(); + c.forEach(spy); + assertTrue(found.isEmpty()); + } + + /** TODO: promote to a common utility */ + static T chooseOne(T ... ts) { + return ts[ThreadLocalRandom.current().nextInt(ts.length)]; + } + + /** TODO: more random adders and removers */ + static Runnable adderRemover(Collection c, E e) { + return chooseOne( + () -> { + assertTrue(c.add(e)); + assertTrue(c.contains(e)); + assertTrue(c.remove(e)); + assertFalse(c.contains(e)); + }, + () -> { + assertTrue(c.add(e)); + assertTrue(c.contains(e)); + assertTrue(c.removeIf(x -> x == e)); + assertFalse(c.contains(e)); + }, + () -> { + assertTrue(c.add(e)); + assertTrue(c.contains(e)); + for (Iterator it = c.iterator();; ) { + if (it.next() == e) { + try { it.remove(); } + catch (UnsupportedOperationException ok) { + c.remove(e); + } + break; + } + } + assertFalse(c.contains(e)); + }); + } + + /** + * Concurrent Spliterators, once exhausted, stay exhausted. + */ + public void testStickySpliteratorExhaustion() throws Throwable { + if (!impl.isConcurrent()) { + return; + } + if (!testImplementationDetails) { + return; + } + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + final Consumer alwaysThrows = e -> { throw new AssertionError(); }; + final Collection c = impl.emptyCollection(); + final Spliterator s = c.spliterator(); + if (rnd.nextBoolean()) { + assertFalse(s.tryAdvance(alwaysThrows)); + } else { + s.forEachRemaining(alwaysThrows); + } + final Object one = impl.makeElement(1); + // Spliterator should not notice added element + c.add(one); + if (rnd.nextBoolean()) { + assertFalse(s.tryAdvance(alwaysThrows)); + } else { + s.forEachRemaining(alwaysThrows); + } + } + + /** + * Motley crew of threads concurrently randomly hammer the collection. + */ + public void testDetectRaces() throws Throwable { + if (!impl.isConcurrent()) { + return; + } + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + final Collection c = impl.emptyCollection(); + final long testDurationMillis + = expensiveTests ? LONG_DELAY_MS : timeoutMillis(); + final AtomicBoolean done = new AtomicBoolean(false); + final Object one = impl.makeElement(1); + final Object two = impl.makeElement(2); + final Consumer checkSanity = x -> assertTrue(x == one || x == two); + final Consumer checkArraySanity = array -> { + // assertTrue(array.length <= 2); // duplicates are permitted + for (Object x : array) { + assertTrue(x == one || x == two); + } + }; + final Object[] emptyArray = + (Object[]) java.lang.reflect.Array.newInstance(one.getClass(), 0); + final List> futures; + final Phaser threadsStarted = new Phaser(1); // register this thread + final Runnable[] frobbers = { + () -> c.forEach(checkSanity), + () -> c.stream().forEach(checkSanity), + () -> c.parallelStream().forEach(checkSanity), + () -> c.spliterator().trySplit(), + () -> { + Spliterator s = c.spliterator(); + s.tryAdvance(checkSanity); + s.trySplit(); + }, + () -> { + Spliterator s = c.spliterator(); + do {} while (s.tryAdvance(checkSanity)); + }, + () -> { for (Object x : c) { + checkSanity.accept(x); + } }, + () -> checkArraySanity.accept(c.toArray()), + () -> checkArraySanity.accept(c.toArray(emptyArray)), + () -> { + Object[] a = new Object[5]; + Object three = impl.makeElement(3); + Arrays.fill(a, 0, a.length, three); + Object[] x = c.toArray(a); + if (x == a) { + for (int i = 0; i < a.length && a[i] != null; i++) + { + checkSanity.accept(a[i]); + // A careful reading of the spec does not support: + // for (i++; i < a.length; i++) assertSame(three, a[i]); + } + } else { + checkArraySanity.accept(x); + } + }, + adderRemover(c, one), + adderRemover(c, two), + }; + final List tasks = + Arrays.stream(frobbers) + .filter(task -> rnd.nextBoolean()) // random subset + .map(task -> (Runnable) () -> { + threadsStarted.arriveAndAwaitAdvance(); + while (!done.get()) { + task.run(); + } + }) + .collect(Collectors.toList()); + final ExecutorService pool = Executors.newCachedThreadPool(); + try (PoolCleaner cleaner = cleaner(pool, done)) { + threadsStarted.bulkRegister(tasks.size()); + futures = tasks.stream() + .map(pool::submit) + .collect(Collectors.toList()); + threadsStarted.arriveAndDeregister(); + Thread.sleep(testDurationMillis); + } + for (Future future : futures) { + assertNull(future.get(0L, MILLISECONDS)); + } + } + + /** + * Spliterators are either IMMUTABLE or truly late-binding or, if + * concurrent, use the same "late-binding style" of returning + * elements added between creation and first use. + */ + public void testLateBindingStyle() { + if (!testImplementationDetails) { + return; + } + if (impl.klazz() == ArrayList.class) + { + return; // for jdk8 + } + // Immutable (snapshot) spliterators are exempt + if (impl.emptyCollection().spliterator() + .hasCharacteristics(Spliterator.IMMUTABLE)) { + return; + } + final Object one = impl.makeElement(1); + { + final Collection c = impl.emptyCollection(); + final Spliterator split = c.spliterator(); + c.add(one); + assertTrue(split.tryAdvance(e -> { assertSame(e, one); })); + assertFalse(split.tryAdvance(e -> { throw new AssertionError(); })); + assertTrue(c.contains(one)); + } + { + final AtomicLong count = new AtomicLong(0); + final Collection c = impl.emptyCollection(); + final Spliterator split = c.spliterator(); + c.add(one); + split.forEachRemaining( + e -> { assertSame(e, one); count.getAndIncrement(); }); + mustEqual(1L, count.get()); + assertFalse(split.tryAdvance(e -> { throw new AssertionError(); })); + assertTrue(c.contains(one)); + } + } + + /** + * Spliterator.getComparator throws IllegalStateException iff the + * spliterator does not report SORTED. + */ + public void testGetComparator_IllegalStateException() { + Collection c = impl.emptyCollection(); + Spliterator s = c.spliterator(); + boolean reportsSorted = s.hasCharacteristics(Spliterator.SORTED); + try { + s.getComparator(); + assertTrue(reportsSorted); + } catch (IllegalStateException ex) { + assertFalse(reportsSorted); + } + } + + public void testCollectionCopies() throws Exception { + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + Collection c = impl.emptyCollection(); + for (int n = rnd.nextInt(4); n--> 0; ) { + c.add(impl.makeElement(rnd.nextInt())); + } + mustEqual(c, c); + if (c instanceof List) { + assertCollectionsEquals(c, new ArrayList(c)); + } else if (c instanceof Set) { + assertCollectionsEquals(c, new HashSet(c)); + } else if (c instanceof Deque) { + assertCollectionsEquivalent(c, new ArrayDeque(c)); + } + + Collection clone = cloneableClone(c); + if (clone != null) { + assertSame(c.getClass(), clone.getClass()); + assertCollectionsEquivalent(c, clone); + } + try { + Collection serialClone = serialClonePossiblyFailing(c); + assertSame(c.getClass(), serialClone.getClass()); + assertCollectionsEquivalent(c, serialClone); + } catch (java.io.NotSerializableException acceptable) {} + } + + /** + * TODO: move out of limbo + * 8203662: remove increment of modCount from ArrayList and Vector replaceAll() + */ + public void DISABLED_testReplaceAllIsNotStructuralModification() { + Collection c = impl.emptyCollection(); + if (!(c instanceof List)) { + return; + } + List list = (List) c; + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + for (int n = rnd.nextInt(2, 10); n--> 0; ) { + list.add(impl.makeElement(rnd.nextInt())); + } + ArrayList copy = new ArrayList(list); + int size = list.size(), half = size / 2; + Iterator it = list.iterator(); + for (int i = 0; i < half; i++) { + mustEqual(it.next(), copy.get(i)); + } + list.replaceAll(n -> n); + // ConcurrentModificationException must not be thrown here. + for (int i = half; i < size; i++) { + mustEqual(it.next(), copy.get(i)); + } + } + +// public void testCollection8DebugFail() { +// fail(impl.klazz().getSimpleName()); +// } +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionImplementation.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionImplementation.java new file mode 100644 index 0000000000..28c1da60ab --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionImplementation.java @@ -0,0 +1,21 @@ +/* + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import java.util.Collection; + +/** Allows tests to work with different Collection implementations. */ +@SuppressWarnings("rawtypes") +public interface CollectionImplementation { + /** Returns the Collection class. */ + public Class klazz(); + /** Returns an empty collection. */ + public Collection emptyCollection(); + public Object makeElement(int i); + public boolean isConcurrent(); + public boolean permitsNulls(); +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionTest.java new file mode 100644 index 0000000000..a0872ca7d3 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/CollectionTest.java @@ -0,0 +1,36 @@ +/* + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import junit.framework.Test; + +/** + * Contains tests applicable to all Collection implementations. + */ +public class CollectionTest extends JSR166TestCase { + final CollectionImplementation impl; + + /** Tests are parameterized by a Collection implementation. */ + CollectionTest(CollectionImplementation impl, String methodName) { + super(methodName); + this.impl = impl; + } + + public static Test testSuite(CollectionImplementation impl) { + return newTestSuite + (parameterizedTestSuite(CollectionTest.class, + CollectionImplementation.class, + impl), + jdk8ParameterizedTestSuite(CollectionTest.class, + CollectionImplementation.class, + impl)); + } + +// public void testCollectionDebugFail() { +// fail(impl.klazz().getSimpleName()); +// } +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMap8Test.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMap8Test.java index 5462b9c409..6b5ace0abd 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMap8Test.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMap8Test.java @@ -9,45 +9,48 @@ import static java.util.Spliterator.DISTINCT; import static java.util.Spliterator.NONNULL; +import java.util.AbstractMap; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.Spliterator; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.LongAdder; +import java.util.function.BiFunction; +import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import junit.framework.Test; import junit.framework.TestSuite; -@SuppressWarnings({"rawtypes", "unchecked", "PreferredInterfaceType"}) +@SuppressWarnings({"rawtypes", "try", "unchecked", "PreferredInterfaceType"}) public class ConcurrentHashMap8Test extends JSR166TestCase { public static void main(String[] args) { - junit.textui.TestRunner.run(suite()); + main(suite(), args); } public static Test suite() { return new TestSuite(ConcurrentHashMap8Test.class); } - private static ConcurrentMap map() { - return Caffeine.newBuilder() - .maximumSize(Integer.MAX_VALUE) - .build().asMap(); - } - - private static Set set() { - return Collections.newSetFromMap(map()); + private static ConcurrentMap bounded() { + Cache cache = Caffeine.newBuilder() + .maximumSize(Integer.MAX_VALUE) + .build(); + return cache.asMap(); } /** - * Returns a new map from Integers 1-5 to Strings "A"-"E". + * Returns a new map from Items 1-5 to Strings "A"-"E". */ - private static ConcurrentMap map5() { - ConcurrentMap map = map(); + private static ConcurrentMap map5() { + ConcurrentMap map = bounded(); assertTrue(map.isEmpty()); map.put(one, "A"); map.put(two, "B"); @@ -55,7 +58,7 @@ private static ConcurrentMap map5() { map.put(four, "D"); map.put(five, "E"); assertFalse(map.isEmpty()); - assertEquals(5, map.size()); + mustEqual(5, map.size()); return map; } @@ -63,17 +66,17 @@ private static ConcurrentMap map5() { * getOrDefault returns value if present, else default */ public void testGetOrDefault() { - ConcurrentMap map = map5(); - assertEquals(map.getOrDefault(one, "Z"), "A"); - assertEquals(map.getOrDefault(six, "Z"), "Z"); + ConcurrentMap map = map5(); + mustEqual(map.getOrDefault(one, "Z"), "A"); + mustEqual(map.getOrDefault(six, "Z"), "Z"); } /** * computeIfAbsent adds when the given key is not present */ public void testComputeIfAbsent() { - ConcurrentMap map = map5(); - map.computeIfAbsent(six, (x) -> "Z"); + ConcurrentMap map = map5(); + map.computeIfAbsent(six, x -> "Z"); assertTrue(map.containsKey(six)); } @@ -81,16 +84,16 @@ public void testComputeIfAbsent() { * computeIfAbsent does not replace if the key is already present */ public void testComputeIfAbsent2() { - ConcurrentMap map = map5(); - assertEquals("A", map.computeIfAbsent(one, (x) -> "Z")); + ConcurrentMap map = map5(); + mustEqual("A", map.computeIfAbsent(one, x -> "Z")); } /** * computeIfAbsent does not add if function returns null */ public void testComputeIfAbsent3() { - ConcurrentMap map = map5(); - map.computeIfAbsent(six, (x) -> null); + ConcurrentMap map = map5(); + map.computeIfAbsent(six, x -> null); assertFalse(map.containsKey(six)); } @@ -98,7 +101,7 @@ public void testComputeIfAbsent3() { * computeIfPresent does not replace if the key is already present */ public void testComputeIfPresent() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.computeIfPresent(six, (x, y) -> "Z"); assertFalse(map.containsKey(six)); } @@ -107,15 +110,15 @@ public void testComputeIfPresent() { * computeIfPresent adds when the given key is not present */ public void testComputeIfPresent2() { - ConcurrentMap map = map5(); - assertEquals("Z", map.computeIfPresent(one, (x, y) -> "Z")); + ConcurrentMap map = map5(); + mustEqual("Z", map.computeIfPresent(one, (x, y) -> "Z")); } /** * compute does not replace if the function returns null */ public void testCompute() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.compute(six, (x, y) -> null); assertFalse(map.containsKey(six)); } @@ -124,23 +127,23 @@ public void testCompute() { * compute adds when the given key is not present */ public void testCompute2() { - ConcurrentMap map = map5(); - assertEquals("Z", map.compute(six, (x, y) -> "Z")); + ConcurrentMap map = map5(); + mustEqual("Z", map.compute(six, (x, y) -> "Z")); } /** * compute replaces when the given key is present */ public void testCompute3() { - ConcurrentMap map = map5(); - assertEquals("Z", map.compute(one, (x, y) -> "Z")); + ConcurrentMap map = map5(); + mustEqual("Z", map.compute(one, (x, y) -> "Z")); } /** * compute removes when the given key is present and function returns null */ public void testCompute4() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.compute(one, (x, y) -> null); assertFalse(map.containsKey(one)); } @@ -149,46 +152,46 @@ public void testCompute4() { * merge adds when the given key is not present */ public void testMerge1() { - ConcurrentMap map = map5(); - assertEquals("Y", map.merge(six, "Y", (x, y) -> "Z")); + ConcurrentMap map = map5(); + mustEqual("Y", map.merge(six, "Y", (x, y) -> "Z")); } /** * merge replaces when the given key is present */ public void testMerge2() { - ConcurrentMap map = map5(); - assertEquals("Z", map.merge(one, "Y", (x, y) -> "Z")); + ConcurrentMap map = map5(); + mustEqual("Z", map.merge(one, "Y", (x, y) -> "Z")); } /** * merge removes when the given key is present and function returns null */ public void testMerge3() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.merge(one, "Y", (x, y) -> null); assertFalse(map.containsKey(one)); } - static Set populatedSet(int n) { - Set a = set(); + static Set populatedSet(int n) { + Set a = Collections.newSetFromMap(bounded()); assertTrue(a.isEmpty()); for (int i = 0; i < n; i++) { - assertTrue(a.add(i)); + mustAdd(a, i); } - assertEquals(n == 0, a.isEmpty()); - assertEquals(n, a.size()); + mustEqual(n == 0, a.isEmpty()); + mustEqual(n, a.size()); return a; } - static Set populatedSet(Integer[] elements) { - Set a = set(); + static Set populatedSet(Item[] elements) { + Set a = Collections.newSetFromMap(bounded()); assertTrue(a.isEmpty()); - for (int i = 0; i < elements.length; i++) { - assertTrue(a.add(elements[i])); + for (Item element : elements) { + assertTrue(a.add(element)); } assertFalse(a.isEmpty()); - assertEquals(elements.length, a.size()); + mustEqual(elements.length, a.size()); return a; } @@ -196,20 +199,20 @@ static Set populatedSet(Integer[] elements) { * replaceAll replaces all matching values. */ public void testReplaceAll() { - ConcurrentMap map = map5(); - map.replaceAll((x, y) -> { return x > 3 ? "Z" : y; }); - assertEquals("A", map.get(one)); - assertEquals("B", map.get(two)); - assertEquals("C", map.get(three)); - assertEquals("Z", map.get(four)); - assertEquals("Z", map.get(five)); + ConcurrentMap map = map5(); + map.replaceAll((x, y) -> (x.value > 3) ? "Z" : y); + mustEqual("A", map.get(one)); + mustEqual("B", map.get(two)); + mustEqual("C", map.get(three)); + mustEqual("Z", map.get(four)); + mustEqual("Z", map.get(five)); } /** * Default-constructed set is empty */ public void testNewKeySet() { - Set a = set(); + Set a = Collections.newSetFromMap(bounded()); assertTrue(a.isEmpty()); } @@ -233,11 +236,11 @@ public void testKeySetAddRemove() { * keySet.addAll adds each element from the given collection */ public void testAddAll() { - Set full = populatedSet(3); + Set full = populatedSet(3); assertTrue(full.addAll(Arrays.asList(three, four, five))); - assertEquals(6, full.size()); + mustEqual(6, full.size()); assertFalse(full.addAll(Arrays.asList(three, four, five))); - assertEquals(6, full.size()); + mustEqual(6, full.size()); } /** @@ -245,32 +248,32 @@ public void testAddAll() { * already exist in the set */ public void testAddAll2() { - Set full = populatedSet(3); + Set full = populatedSet(3); // "one" is duplicate and will not be added assertTrue(full.addAll(Arrays.asList(three, four, one))); - assertEquals(5, full.size()); + mustEqual(5, full.size()); assertFalse(full.addAll(Arrays.asList(three, four, one))); - assertEquals(5, full.size()); + mustEqual(5, full.size()); } /** * keySet.add will not add the element if it already exists in the set */ public void testAdd2() { - Set full = populatedSet(3); + Set full = populatedSet(3); assertFalse(full.add(one)); - assertEquals(3, full.size()); + mustEqual(3, full.size()); } /** * keySet.add adds the element when it does not exist in the set */ public void testAdd3() { - Set full = populatedSet(3); + Set full = populatedSet(3); assertTrue(full.add(three)); - assertTrue(full.contains(three)); + mustContain(full, three); assertFalse(full.add(three)); - assertTrue(full.contains(three)); + mustContain(full, three); } /** @@ -278,7 +281,7 @@ public void testAdd3() { * mapped value */ public void testAdd4() { - Set full = map5().keySet(); + Set full = map5().keySet(); try { full.add(three); shouldThrow(); @@ -290,16 +293,37 @@ public void testAdd4() { * null */ public void testAdd5() { - Set full = populatedSet(3); + Set full = populatedSet(3); try { full.add(null); shouldThrow(); } catch (NullPointerException success) {} } +// /** +// * KeySetView.getMappedValue returns the map's mapped value +// */ +// public void testGetMappedValue() { +// ConcurrentHashMap map = map5(); +// assertNull(map.keySet().getMappedValue()); +// String added = "added"; +// try { +// map.keySet(null); +// shouldThrow(); +// } catch (NullPointerException success) {} +// ConcurrentHashMap.KeySetView set = map.keySet(added); +// assertFalse(set.add(one)); +// assertTrue(set.add(six)); +// assertTrue(set.add(seven)); +// assertSame(added, set.getMappedValue()); +// assertNotSame(added, map.get(one)); +// assertSame(added, map.get(six)); +// assertSame(added, map.get(seven)); +// } + void checkSpliteratorCharacteristics(Spliterator sp, int requiredCharacteristics) { - assertEquals(requiredCharacteristics, + mustEqual(requiredCharacteristics, requiredCharacteristics & sp.characteristics()); } @@ -308,60 +332,60 @@ void checkSpliteratorCharacteristics(Spliterator sp, */ public void testKeySetSpliterator() { LongAdder adder = new LongAdder(); - ConcurrentMap map = map5(); - Set set = map.keySet(); - Spliterator sp = set.spliterator(); + ConcurrentMap map = map5(); + Set set = map.keySet(); + Spliterator sp = set.spliterator(); checkSpliteratorCharacteristics(sp, CONCURRENT | DISTINCT | NONNULL); - assertEquals(sp.estimateSize(), map.size()); - Spliterator sp2 = sp.trySplit(); - sp.forEachRemaining((Integer x) -> adder.add(x.longValue())); + mustEqual(sp.estimateSize(), map.size()); + Spliterator sp2 = sp.trySplit(); + sp.forEachRemaining((Item x) -> adder.add(x.longValue())); long v = adder.sumThenReset(); - sp2.forEachRemaining((Integer x) -> adder.add(x.longValue())); + sp2.forEachRemaining((Item x) -> adder.add(x.longValue())); long v2 = adder.sum(); - assertEquals(v + v2, 15); + mustEqual(v + v2, 15); } /** * keyset.clear removes all elements from the set */ public void testClear() { - Set full = populatedSet(3); + Set full = populatedSet(3); full.clear(); - assertEquals(0, full.size()); + mustEqual(0, full.size()); } /** * keyset.contains returns true for added elements */ public void testContains() { - Set full = populatedSet(3); - assertTrue(full.contains(one)); - assertFalse(full.contains(five)); + Set full = populatedSet(3); + mustContain(full, one); + mustNotContain(full, five); } /** * KeySets with equal elements are equal */ public void testEquals() { - Set a = populatedSet(3); - Set b = populatedSet(3); + Set a = populatedSet(3); + Set b = populatedSet(3); assertTrue(a.equals(b)); assertTrue(b.equals(a)); - assertEquals(a.hashCode(), b.hashCode()); - a.add(m1); + mustEqual(a.hashCode(), b.hashCode()); + a.add(minusOne); assertFalse(a.equals(b)); assertFalse(b.equals(a)); - b.add(m1); + b.add(minusOne); assertTrue(a.equals(b)); assertTrue(b.equals(a)); - assertEquals(a.hashCode(), b.hashCode()); + mustEqual(a.hashCode(), b.hashCode()); } /** * KeySet.containsAll returns true for collections with subset of elements */ public void testContainsAll() { - Collection full = populatedSet(3); + Collection full = populatedSet(3); assertTrue(full.containsAll(Arrays.asList())); assertTrue(full.containsAll(Arrays.asList(one))); assertTrue(full.containsAll(Arrays.asList(one, two))); @@ -382,7 +406,7 @@ public void testIsEmpty() { * set */ public void testIterator() { - Collection empty = set(); + Collection empty = Collections.newSetFromMap(bounded()); int size = 20; assertFalse(empty.iterator().hasNext()); try { @@ -390,14 +414,11 @@ public void testIterator() { shouldThrow(); } catch (NoSuchElementException success) {} - Integer[] elements = new Integer[size]; - for (int i = 0; i < size; i++) { - elements[i] = i; - } - Collections.shuffle(Arrays.asList(elements)); - Collection full = populatedSet(elements); + Item[] elements = seqItems(size); + shuffle(elements); + Collection full = populatedSet(elements); - Iterator it = full.iterator(); + Iterator it = full.iterator(); for (int j = 0; j < size; j++) { assertTrue(it.hasNext()); it.next(); @@ -409,18 +430,18 @@ public void testIterator() { * iterator of empty collections has no elements */ public void testEmptyIterator() { - assertIteratorExhausted(set().iterator()); - assertIteratorExhausted(map().entrySet().iterator()); - assertIteratorExhausted(map().values().iterator()); - assertIteratorExhausted(map().keySet().iterator()); + assertIteratorExhausted(Collections.newSetFromMap(bounded()).iterator()); + assertIteratorExhausted(bounded().entrySet().iterator()); + assertIteratorExhausted(bounded().values().iterator()); + assertIteratorExhausted(bounded().keySet().iterator()); } /** * KeySet.iterator.remove removes current element */ public void testIteratorRemove() { - Set q = populatedSet(3); - Iterator it = q.iterator(); + Set q = populatedSet(3); + Iterator it = q.iterator(); Object removed = it.next(); it.remove(); @@ -434,8 +455,8 @@ public void testIteratorRemove() { * KeySet.toString holds toString of elements */ public void testToString() { - assertEquals("[]", set().toString()); - Set full = populatedSet(3); + mustEqual("[]", bounded().keySet().toString()); + Set full = populatedSet(3); String s = full.toString(); for (int i = 0; i < 3; ++i) { assertTrue(s.contains(String.valueOf(i))); @@ -446,31 +467,31 @@ public void testToString() { * KeySet.removeAll removes all elements from the given collection */ public void testRemoveAll() { - Set full = populatedSet(3); + Set full = populatedSet(3); assertTrue(full.removeAll(Arrays.asList(one, two))); - assertEquals(1, full.size()); + mustEqual(1, full.size()); assertFalse(full.removeAll(Arrays.asList(one, two))); - assertEquals(1, full.size()); + mustEqual(1, full.size()); } /** * KeySet.remove removes an element */ public void testRemove() { - Set full = populatedSet(3); + Set full = populatedSet(3); full.remove(one); - assertFalse(full.contains(one)); - assertEquals(2, full.size()); + mustNotContain(full, one); + mustEqual(2, full.size()); } /** * keySet.size returns the number of elements */ public void testSize() { - Set empty = set(); - Set full = populatedSet(3); - assertEquals(3, full.size()); - assertEquals(0, empty.size()); + Set empty = Collections.newSetFromMap(bounded()); + Set full = populatedSet(3); + mustEqual(3, full.size()); + mustEqual(0, empty.size()); } /** @@ -478,16 +499,13 @@ public void testSize() { * the set */ public void testToArray() { - Object[] a = set().toArray(); + Object[] a = Collections.newSetFromMap(bounded()).toArray(); assertTrue(Arrays.equals(new Object[0], a)); assertSame(Object[].class, a.getClass()); int size = 20; - Integer[] elements = new Integer[size]; - for (int i = 0; i < size; i++) { - elements[i] = i; - } - Collections.shuffle(Arrays.asList(elements)); - Collection full = populatedSet(elements); + Item[] elements = seqItems(size); + shuffle(elements); + Collection full = populatedSet(elements); assertTrue(Arrays.asList(elements).containsAll(Arrays.asList(full.toArray()))); assertTrue(full.containsAll(Arrays.asList(full.toArray()))); @@ -495,42 +513,621 @@ public void testToArray() { } /** - * toArray(Integer array) returns an Integer array containing all + * toArray(Item array) returns an Item array containing all * elements from the set */ public void testToArray2() { - Collection empty = set(); - Integer[] a; + Collection empty = Collections.newSetFromMap(bounded()); + Item[] a; int size = 20; - a = new Integer[0]; + a = new Item[0]; assertSame(a, empty.toArray(a)); - a = new Integer[size/2]; - Arrays.fill(a, 42); + a = new Item[size / 2]; + Arrays.fill(a, fortytwo); assertSame(a, empty.toArray(a)); assertNull(a[0]); for (int i = 1; i < a.length; i++) { - assertEquals(42, (int) a[i]); + mustEqual(42, a[i]); } - Integer[] elements = new Integer[size]; - for (int i = 0; i < size; i++) { - elements[i] = i; - } - Collections.shuffle(Arrays.asList(elements)); - Collection full = populatedSet(elements); + Item[] elements = seqItems(size); + shuffle(elements); + Collection full = populatedSet(elements); - Arrays.fill(a, 42); + Arrays.fill(a, fortytwo); assertTrue(Arrays.asList(elements).containsAll(Arrays.asList(full.toArray(a)))); for (int i = 0; i < a.length; i++) { - assertEquals(42, (int) a[i]); + mustEqual(42, a[i]); } - assertSame(Integer[].class, full.toArray(a).getClass()); + assertSame(Item[].class, full.toArray(a).getClass()); - a = new Integer[size]; - Arrays.fill(a, 42); + a = new Item[size]; + Arrays.fill(a, fortytwo); assertSame(a, full.toArray(a)); assertTrue(Arrays.asList(elements).containsAll(Arrays.asList(full.toArray(a)))); } + +// /** +// * A deserialized/reserialized set equals original +// */ +// public void testSerialization() throws Exception { +// int size = 20; +// Set x = populatedSet(size); +// Set y = serialClone(x); +// +// assertNotSame(x, y); +// mustEqual(x.size(), y.size()); +// mustEqual(x, y); +// mustEqual(y, x); +// } + + static final int SIZE = 10000; + static ConcurrentMap longMap; + + static ConcurrentMap longMap() { + if (longMap == null) { + longMap = bounded(); + for (int i = 0; i < SIZE; ++i) { + longMap.put(Long.valueOf(i), Long.valueOf(2 *i)); + } + } + return longMap; + } + + // explicit function class to avoid type inference problems + static class AddKeys implements BiFunction, Map.Entry, Map.Entry> { + @Override + public Map.Entry apply(Map.Entry x, Map.Entry y) { + return new AbstractMap.SimpleEntry + (Long.valueOf(x.getKey().longValue() + y.getKey().longValue()), + Long.valueOf(1L)); + } + } +// +// /** +// * forEachKeySequentially traverses all keys +// */ +// public void testForEachKeySequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachKey(Long.MAX_VALUE, (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * forEachValueSequentially traverses all values +// */ +// public void testForEachValueSequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachValue(Long.MAX_VALUE, (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), SIZE * (SIZE - 1)); +// } +// +// /** +// * forEachSequentially traverses all mappings +// */ +// public void testForEachSequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEach(Long.MAX_VALUE, (Long x, Long y) -> adder.add(x.longValue() + y.longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * forEachEntrySequentially traverses all entries +// */ +// public void testForEachEntrySequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachEntry(Long.MAX_VALUE, (Map.Entry e) -> adder.add(e.getKey().longValue() + e.getValue().longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * forEachKeyInParallel traverses all keys +// */ +// public void testForEachKeyInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachKey(1L, (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * forEachValueInParallel traverses all values +// */ +// public void testForEachValueInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachValue(1L, (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), SIZE * (SIZE - 1)); +// } +// +// /** +// * forEachInParallel traverses all mappings +// */ +// public void testForEachInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEach(1L, (Long x, Long y) -> adder.add(x.longValue() + y.longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * forEachEntryInParallel traverses all entries +// */ +// public void testForEachEntryInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachEntry(1L, (Map.Entry e) -> adder.add(e.getKey().longValue() + e.getValue().longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped forEachKeySequentially traverses the given +// * transformations of all keys +// */ +// public void testMappedForEachKeySequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachKey(Long.MAX_VALUE, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 4 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped forEachValueSequentially traverses the given +// * transformations of all values +// */ +// public void testMappedForEachValueSequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachValue(Long.MAX_VALUE, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 4 * SIZE * (SIZE - 1)); +// } +// +// /** +// * Mapped forEachSequentially traverses the given +// * transformations of all mappings +// */ +// public void testMappedForEachSequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEach(Long.MAX_VALUE, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped forEachEntrySequentially traverses the given +// * transformations of all entries +// */ +// public void testMappedForEachEntrySequentially() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachEntry(Long.MAX_VALUE, (Map.Entry e) -> Long.valueOf(e.getKey().longValue() + e.getValue().longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped forEachKeyInParallel traverses the given +// * transformations of all keys +// */ +// public void testMappedForEachKeyInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachKey(1L, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 4 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped forEachValueInParallel traverses the given +// * transformations of all values +// */ +// public void testMappedForEachValueInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachValue(1L, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 4 * SIZE * (SIZE - 1)); +// } +// +// /** +// * Mapped forEachInParallel traverses the given +// * transformations of all mappings +// */ +// public void testMappedForEachInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEach(1L, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped forEachEntryInParallel traverses the given +// * transformations of all entries +// */ +// public void testMappedForEachEntryInParallel() { +// LongAdder adder = new LongAdder(); +// ConcurrentMap m = longMap(); +// m.forEachEntry(1L, (Map.Entry e) -> Long.valueOf(e.getKey().longValue() + e.getValue().longValue()), +// (Long x) -> adder.add(x.longValue())); +// mustEqual(adder.sum(), 3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysSequentially accumulates across all keys, +// */ +// public void testReduceKeysSequentially() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.reduceKeys(Long.MAX_VALUE, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceValuesSequentially accumulates across all values +// */ +// public void testReduceValuesSequentially() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.reduceKeys(Long.MAX_VALUE, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceEntriesSequentially accumulates across all entries +// */ +// public void testReduceEntriesSequentially() { +// ConcurrentMap m = longMap(); +// Map.Entry r; +// r = m.reduceEntries(Long.MAX_VALUE, new AddKeys()); +// mustEqual(r.getKey().longValue(), (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysInParallel accumulates across all keys +// */ +// public void testReduceKeysInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.reduceKeys(1L, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceValuesInParallel accumulates across all values +// */ +// public void testReduceValuesInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.reduceValues(1L, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceEntriesInParallel accumulate across all entries +// */ +// public void testReduceEntriesInParallel() { +// ConcurrentMap m = longMap(); +// Map.Entry r; +// r = m.reduceEntries(1L, new AddKeys()); +// mustEqual(r.getKey().longValue(), (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped reduceKeysSequentially accumulates mapped keys +// */ +// public void testMapReduceKeysSequentially() { +// ConcurrentMap m = longMap(); +// Long r = m.reduceKeys(Long.MAX_VALUE, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)4 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped reduceValuesSequentially accumulates mapped values +// */ +// public void testMapReduceValuesSequentially() { +// ConcurrentMap m = longMap(); +// Long r = m.reduceValues(Long.MAX_VALUE, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)4 * SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceSequentially accumulates across all transformed mappings +// */ +// public void testMappedReduceSequentially() { +// ConcurrentMap m = longMap(); +// Long r = m.reduce(Long.MAX_VALUE, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue()), +// (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// +// mustEqual((long)r, (long)3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped reduceKeysInParallel, accumulates mapped keys +// */ +// public void testMapReduceKeysInParallel() { +// ConcurrentMap m = longMap(); +// Long r = m.reduceKeys(1L, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)4 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * Mapped reduceValuesInParallel accumulates mapped values +// */ +// public void testMapReduceValuesInParallel() { +// ConcurrentMap m = longMap(); +// Long r = m.reduceValues(1L, (Long x) -> Long.valueOf(4 * x.longValue()), +// (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)4 * SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceInParallel accumulate across all transformed mappings +// */ +// public void testMappedReduceInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.reduce(1L, (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue()), +// (Long x, Long y) -> Long.valueOf(x.longValue() + y.longValue())); +// mustEqual((long)r, (long)3 * SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysToLongSequentially accumulates mapped keys +// */ +// public void testReduceKeysToLongSequentially() { +// ConcurrentMap m = longMap(); +// long lr = m.reduceKeysToLong(Long.MAX_VALUE, (Long x) -> x.longValue(), 0L, Long::sum); +// mustEqual(lr, (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysToIntSequentially accumulates mapped keys +// */ +// public void testReduceKeysToIntSequentially() { +// ConcurrentMap m = longMap(); +// int ir = m.reduceKeysToInt(Long.MAX_VALUE, (Long x) -> x.intValue(), 0, Integer::sum); +// mustEqual(ir, SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysToDoubleSequentially accumulates mapped keys +// */ +// public void testReduceKeysToDoubleSequentially() { +// ConcurrentMap m = longMap(); +// double dr = m.reduceKeysToDouble(Long.MAX_VALUE, (Long x) -> x.doubleValue(), 0.0, Double::sum); +// mustEqual(dr, (double)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceValuesToLongSequentially accumulates mapped values +// */ +// public void testReduceValuesToLongSequentially() { +// ConcurrentMap m = longMap(); +// long lr = m.reduceValuesToLong(Long.MAX_VALUE, (Long x) -> x.longValue(), 0L, Long::sum); +// mustEqual(lr, (long)SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceValuesToIntSequentially accumulates mapped values +// */ +// public void testReduceValuesToIntSequentially() { +// ConcurrentMap m = longMap(); +// int ir = m.reduceValuesToInt(Long.MAX_VALUE, (Long x) -> x.intValue(), 0, Integer::sum); +// mustEqual(ir, SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceValuesToDoubleSequentially accumulates mapped values +// */ +// public void testReduceValuesToDoubleSequentially() { +// ConcurrentMap m = longMap(); +// double dr = m.reduceValuesToDouble(Long.MAX_VALUE, (Long x) -> x.doubleValue(), 0.0, Double::sum); +// mustEqual(dr, (double)SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceKeysToLongInParallel accumulates mapped keys +// */ +// public void testReduceKeysToLongInParallel() { +// ConcurrentMap m = longMap(); +// long lr = m.reduceKeysToLong(1L, (Long x) -> x.longValue(), 0L, Long::sum); +// mustEqual(lr, (long)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysToIntInParallel accumulates mapped keys +// */ +// public void testReduceKeysToIntInParallel() { +// ConcurrentMap m = longMap(); +// int ir = m.reduceKeysToInt(1L, (Long x) -> x.intValue(), 0, Integer::sum); +// mustEqual(ir, SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceKeysToDoubleInParallel accumulates mapped values +// */ +// public void testReduceKeysToDoubleInParallel() { +// ConcurrentMap m = longMap(); +// double dr = m.reduceKeysToDouble(1L, (Long x) -> x.doubleValue(), 0.0, Double::sum); +// mustEqual(dr, (double)SIZE * (SIZE - 1) / 2); +// } +// +// /** +// * reduceValuesToLongInParallel accumulates mapped values +// */ +// public void testReduceValuesToLongInParallel() { +// ConcurrentMap m = longMap(); +// long lr = m.reduceValuesToLong(1L, (Long x) -> x.longValue(), 0L, Long::sum); +// mustEqual(lr, (long)SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceValuesToIntInParallel accumulates mapped values +// */ +// public void testReduceValuesToIntInParallel() { +// ConcurrentMap m = longMap(); +// int ir = m.reduceValuesToInt(1L, (Long x) -> x.intValue(), 0, Integer::sum); +// mustEqual(ir, SIZE * (SIZE - 1)); +// } +// +// /** +// * reduceValuesToDoubleInParallel accumulates mapped values +// */ +// public void testReduceValuesToDoubleInParallel() { +// ConcurrentMap m = longMap(); +// double dr = m.reduceValuesToDouble(1L, (Long x) -> x.doubleValue(), 0.0, Double::sum); +// mustEqual(dr, (double)SIZE * (SIZE - 1)); +// } +// +// /** +// * searchKeysSequentially returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchKeysSequentially() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.searchKeys(Long.MAX_VALUE, (Long x) -> x.longValue() == SIZE/2 ? x : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.searchKeys(Long.MAX_VALUE, (Long x) -> x.longValue() < 0L ? x : null); +// assertNull(r); +// } +// +// /** +// * searchValuesSequentially returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchValuesSequentially() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.searchValues(Long.MAX_VALUE, +// (Long x) -> (x.longValue() == SIZE/2) ? x : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.searchValues(Long.MAX_VALUE, +// (Long x) -> (x.longValue() < 0L) ? x : null); +// assertNull(r); +// } +// +// /** +// * searchSequentially returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchSequentially() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.search(Long.MAX_VALUE, (Long x, Long y) -> x.longValue() == SIZE/2 ? x : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.search(Long.MAX_VALUE, (Long x, Long y) -> x.longValue() < 0L ? x : null); +// assertNull(r); +// } +// +// /** +// * searchEntriesSequentially returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchEntriesSequentially() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.searchEntries(Long.MAX_VALUE, (Map.Entry e) -> e.getKey().longValue() == SIZE/2 ? e.getKey() : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.searchEntries(Long.MAX_VALUE, (Map.Entry e) -> e.getKey().longValue() < 0L ? e.getKey() : null); +// assertNull(r); +// } +// +// /** +// * searchKeysInParallel returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchKeysInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.searchKeys(1L, (Long x) -> x.longValue() == SIZE/2 ? x : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.searchKeys(1L, (Long x) -> x.longValue() < 0L ? x : null); +// assertNull(r); +// } +// +// /** +// * searchValuesInParallel returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchValuesInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.searchValues(1L, (Long x) -> x.longValue() == SIZE/2 ? x : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.searchValues(1L, (Long x) -> x.longValue() < 0L ? x : null); +// assertNull(r); +// } +// +// /** +// * searchInParallel returns a non-null result of search function, +// * or null if none +// */ +// public void testSearchInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.search(1L, (Long x, Long y) -> x.longValue() == SIZE/2 ? x : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.search(1L, (Long x, Long y) -> x.longValue() < 0L ? x : null); +// assertNull(r); +// } +// +// /** +// * searchEntriesInParallel returns a non-null result of search +// * function, or null if none +// */ +// public void testSearchEntriesInParallel() { +// ConcurrentMap m = longMap(); +// Long r; +// r = m.searchEntries(1L, (Map.Entry e) -> e.getKey().longValue() == SIZE/2 ? e.getKey() : null); +// mustEqual((long)r, (long)(SIZE/2)); +// r = m.searchEntries(1L, (Map.Entry e) -> e.getKey().longValue() < 0L ? e.getKey() : null); +// assertNull(r); +// } + + /** + * Tests performance of computeIfAbsent when the element is present. + * See JDK-8161372 + * ant -Djsr166.tckTestClass=ConcurrentHashMapTest -Djsr166.methodFilter=testcomputeIfAbsent_performance -Djsr166.expensiveTests=true tck + */ + public void testcomputeIfAbsent_performance() { + final int mapSize = 20; + final int iterations = expensiveTests ? (1 << 23) : mapSize * 2; + final int threads = expensiveTests ? 10 : 2; + final ConcurrentMap map = bounded(); + for (int i = 0; i < mapSize; i++) { + Item I = itemFor(i); + map.put(I, I); + } + final ExecutorService pool = Executors.newFixedThreadPool(2); + try (PoolCleaner cleaner = cleaner(pool)) { + Runnable r = new CheckedRunnable() { + @Override + public void realRun() { + int result = 0; + for (int i = 0; i < iterations; i++) { + result += map.computeIfAbsent(itemFor(i % mapSize), k -> itemFor(k.value * 2)).value; + } + if (result == -42) { + throw new Error(); + } + }}; + for (int i = 0; i < threads; i++) { + pool.execute(r); + } + } + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMapTest.java index b75aa2396d..7f801bb707 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMapTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/ConcurrentHashMapTest.java @@ -1,6 +1,7 @@ /* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ * Other contributors include Andrew Wright, Jeffrey Hayes, * Pat Fisher, Mike Judd. @@ -15,34 +16,63 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import junit.framework.Test; -import junit.framework.TestSuite; -@SuppressWarnings({"rawtypes", "unchecked", "HashCodeToString", - "ModifyCollectionInEnhancedForLoop", "ReturnValueIgnored", "UnnecessaryParentheses"}) +@SuppressWarnings({"rawtypes", "unchecked", "ForEachIterable", "HashCodeToString", + "ModifyCollectionInEnhancedForLoop", "PreferredInterfaceType", "ReturnValueIgnored", + "UnnecessaryParentheses"}) public class ConcurrentHashMapTest extends JSR166TestCase { public static void main(String[] args) { - junit.textui.TestRunner.run(suite()); + main(suite(), args); } public static Test suite() { - return new TestSuite(ConcurrentHashMapTest.class); + class Implementation implements MapImplementation { + final boolean bounded; + + Implementation(boolean bounded) { this.bounded = bounded; } + + @Override + public Class klazz() { return ConcurrentHashMap.class; } + @Override + public Map emptyMap() { return bounded ? bounded() : unbounded(); } + @Override + public boolean isConcurrent() { return true; } + @Override + public boolean permitsNullKeys() { return false; } + @Override + public boolean permitsNullValues() { return false; } + @Override + public boolean supportsSetValue() { return true; } + } + return newTestSuite( + ConcurrentHashMapTest.class, + MapTest.testSuite(new Implementation(false)), + MapTest.testSuite(new Implementation(true))); } - private static ConcurrentMap map() { - return Caffeine.newBuilder() - .maximumSize(Integer.MAX_VALUE) - .build().asMap(); + private static ConcurrentMap unbounded() { + Cache cache = Caffeine.newBuilder().build(); + return cache.asMap(); } + private static ConcurrentMap bounded() { + Cache cache = Caffeine.newBuilder() + .maximumSize(Integer.MAX_VALUE) + .build(); + return cache.asMap(); + } + /** - * Returns a new map from Integers 1-5 to Strings "A"-"E". + * Returns a new map from Items 1-5 to Strings "A"-"E". */ - private static ConcurrentMap map5() { - ConcurrentMap map = map(); + private static ConcurrentMap map5() { + ConcurrentMap map = bounded(); assertTrue(map.isEmpty()); map.put(one, "A"); map.put(two, "B"); @@ -50,20 +80,17 @@ private static ConcurrentMap map5() { map.put(four, "D"); map.put(five, "E"); assertFalse(map.isEmpty()); - assertEquals(5, map.size()); + mustEqual(5, map.size()); return map; } - /** Re-implement Integer.compare for old java versions */ - static int compare(int x, int y) { return x < y ? -1 : x > y ? 1 : 0; } - // classes for testing Comparable fallbacks static class BI implements Comparable { private final int value; BI(int value) { this.value = value; } @Override public int compareTo(BI other) { - return compare(value, other.value); + return Integer.compare(value, other.value); } @Override public boolean equals(Object x) { @@ -104,7 +131,7 @@ public int compareTo(LexicographicList other) { } } if (r == 0) { - r = compare(size(), other.size()); + r = Integer.compare(size(), other.size()); } return r; } @@ -136,9 +163,9 @@ public int compareTo(final ComparableCollidingObject o) { */ public void testComparableFamily() { int size = 500; // makes measured test run time -> 60ms - ConcurrentMap m = map(); + ConcurrentMap m = bounded(); for (int i = 0; i < size; i++) { - assertTrue(m.put(new CI(i), true) == null); + assertNull(m.put(new CI(i), true)); } for (int i = 0; i < size; i++) { assertTrue(m.containsKey(new CI(i))); @@ -152,13 +179,13 @@ public void testComparableFamily() { */ public void testGenericComparable() { int size = 120; // makes measured test run time -> 60ms - ConcurrentMap m = map(); + ConcurrentMap m = bounded(); for (int i = 0; i < size; i++) { BI bi = new BI(i); BS bs = new BS(String.valueOf(i)); - LexicographicList bis = new LexicographicList(bi); - LexicographicList bss = new LexicographicList(bs); - assertTrue(m.putIfAbsent(bis, true) == null); + LexicographicList bis = new LexicographicList<>(bi); + LexicographicList bss = new LexicographicList<>(bs); + assertNull(m.putIfAbsent(bis, true)); assertTrue(m.containsKey(bis)); if (m.putIfAbsent(bss, true) == null) { assertTrue(m.containsKey(bss)); @@ -177,13 +204,13 @@ public void testGenericComparable() { */ public void testGenericComparable2() { int size = 500; // makes measured test run time -> 60ms - ConcurrentMap m = map(); + ConcurrentMap m = bounded(); for (int i = 0; i < size; i++) { m.put(Collections.singletonList(new BI(i)), true); } for (int i = 0; i < size; i++) { - LexicographicList bis = new LexicographicList(new BI(i)); + LexicographicList bis = new LexicographicList<>(new BI(i)); assertTrue(m.containsKey(bis)); } } @@ -194,7 +221,7 @@ public void testGenericComparable2() { */ public void testMixedComparable() { int size = 1200; // makes measured test run time -> 35ms - ConcurrentMap map = map(); + ConcurrentMap map = bounded(); Random rng = new Random(); for (int i = 0; i < size; i++) { Object x; @@ -212,13 +239,13 @@ public void testMixedComparable() { } int count = 0; for (Object k : map.keySet()) { - assertEquals(map.get(k), k); + mustEqual(map.get(k), k); ++count; } - assertEquals(count, size); - assertEquals(map.size(), size); + mustEqual(count, size); + mustEqual(map.size(), size); for (Object k : map.keySet()) { - assertEquals(map.put(k, k), k); + mustEqual(map.put(k, k), k); } } @@ -226,19 +253,19 @@ public void testMixedComparable() { * clear removes all pairs */ public void testClear() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.clear(); - assertEquals(0, map.size()); + mustEqual(0, map.size()); } /** * Maps with same contents are equal */ public void testEquals() { - ConcurrentMap map1 = map5(); - ConcurrentMap map2 = map5(); - assertEquals(map1, map2); - assertEquals(map2, map1); + ConcurrentMap map1 = map5(); + ConcurrentMap map2 = map5(); + mustEqual(map1, map2); + mustEqual(map2, map1); map1.clear(); assertFalse(map1.equals(map2)); assertFalse(map2.equals(map1)); @@ -248,19 +275,19 @@ public void testEquals() { * hashCode() equals sum of each key.hashCode ^ value.hashCode */ public void testHashCode() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); int sum = 0; - for (Map.Entry e : map.entrySet()) { + for (Map.Entry e : map.entrySet()) { sum += e.getKey().hashCode() ^ e.getValue().hashCode(); } - assertEquals(sum, map.hashCode()); + mustEqual(sum, map.hashCode()); } /** * containsKey returns true for contained key */ public void testContainsKey() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); assertTrue(map.containsKey(one)); assertFalse(map.containsKey(zero)); } @@ -269,19 +296,35 @@ public void testContainsKey() { * containsValue returns true for held values */ public void testContainsValue() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); assertTrue(map.containsValue("A")); assertFalse(map.containsValue("Z")); } +// /** +// * enumeration returns an enumeration containing the correct +// * elements +// */ +// public void testEnumeration() { +// ConcurrentMap map = map5(); +// Enumeration e = map.elements(); +// int count = 0; +// while (e.hasMoreElements()) { +// count++; +// e.nextElement(); +// } +// mustEqual(5, count); +// } + /** * get returns the correct element at the given key, * or null if not present */ + @SuppressWarnings({"CollectionIncompatibleType", "unlikely-arg-type"}) public void testGet() { - ConcurrentMap map = map5(); - assertEquals("A", (String)map.get(one)); - ConcurrentMap empty = map(); + ConcurrentMap map = map5(); + mustEqual("A", map.get(one)); + ConcurrentMap empty = bounded(); assertNull(map.get("anything")); assertNull(empty.get("anything")); } @@ -290,36 +333,65 @@ public void testGet() { * isEmpty is true of empty map and false for non-empty */ public void testIsEmpty() { - ConcurrentMap empty = map(); - ConcurrentMap map = map5(); + ConcurrentMap empty = bounded(); + ConcurrentMap map = map5(); assertTrue(empty.isEmpty()); assertFalse(map.isEmpty()); } +// /** +// * keys returns an enumeration containing all the keys from the map +// */ +// public void testKeys() { +// ConcurrentMap map = map5(); +// Enumeration e = map.keys(); +// int count = 0; +// while (e.hasMoreElements()) { +// count++; +// e.nextElement(); +// } +// mustEqual(5, count); +// } + /** * keySet returns a Set containing all the keys */ public void testKeySet() { - ConcurrentMap map = map5(); - Set s = map.keySet(); - assertEquals(5, s.size()); - assertTrue(s.contains(one)); - assertTrue(s.contains(two)); - assertTrue(s.contains(three)); - assertTrue(s.contains(four)); - assertTrue(s.contains(five)); + ConcurrentMap map = map5(); + Set s = map.keySet(); + mustEqual(5, s.size()); + mustContain(s, one); + mustContain(s, two); + mustContain(s, three); + mustContain(s, four); + mustContain(s, five); + } + + /** + * Test keySet().removeAll on empty map + */ + public void testKeySet_empty_removeAll() { + ConcurrentMap map = bounded(); + Set set = map.keySet(); + set.removeAll(Collections.emptyList()); + assertTrue(map.isEmpty()); + assertTrue(set.isEmpty()); + // following is test for JDK-8163353 + set.removeAll(Collections.emptySet()); + assertTrue(map.isEmpty()); + assertTrue(set.isEmpty()); } /** * keySet.toArray returns contains all keys */ public void testKeySetToArray() { - ConcurrentMap map = map5(); - Set s = map.keySet(); + ConcurrentMap map = map5(); + Set s = map.keySet(); Object[] ar = s.toArray(); assertTrue(s.containsAll(Arrays.asList(ar))); - assertEquals(5, ar.length); - ar[0] = m10; + mustEqual(5, ar.length); + ar[0] = minusTen; assertFalse(s.containsAll(Arrays.asList(ar))); } @@ -327,11 +399,11 @@ public void testKeySetToArray() { * Values.toArray contains all values */ public void testValuesToArray() { - ConcurrentMap map = map5(); - Collection v = map.values(); - Object[] ar = v.toArray(); - ArrayList s = new ArrayList(Arrays.asList(ar)); - assertEquals(5, ar.length); + ConcurrentMap map = map5(); + Collection v = map.values(); + String[] ar = v.toArray(new String[0]); + ArrayList s = new ArrayList<>(Arrays.asList(ar)); + mustEqual(5, ar.length); assertTrue(s.contains("A")); assertTrue(s.contains("B")); assertTrue(s.contains("C")); @@ -343,10 +415,10 @@ public void testValuesToArray() { * entrySet.toArray contains all entries */ public void testEntrySetToArray() { - ConcurrentMap map = map5(); - Set s = map.entrySet(); + ConcurrentMap map = map5(); + Set> s = map.entrySet(); Object[] ar = s.toArray(); - assertEquals(5, ar.length); + mustEqual(5, ar.length); for (int i = 0; i < 5; ++i) { assertTrue(map.containsKey(((Map.Entry)(ar[i])).getKey())); assertTrue(map.containsValue(((Map.Entry)(ar[i])).getValue())); @@ -357,9 +429,9 @@ public void testEntrySetToArray() { * values collection contains all values */ public void testValues() { - ConcurrentMap map = map5(); - Collection s = map.values(); - assertEquals(5, s.size()); + ConcurrentMap map = map5(); + Collection s = map.values(); + mustEqual(5, s.size()); assertTrue(s.contains("A")); assertTrue(s.contains("B")); assertTrue(s.contains("C")); @@ -371,12 +443,12 @@ public void testValues() { * entrySet contains all pairs */ public void testEntrySet() { - ConcurrentMap map = map5(); - Set s = map.entrySet(); - assertEquals(5, s.size()); - Iterator it = s.iterator(); + ConcurrentMap map = map5(); + Set> s = map.entrySet(); + mustEqual(5, s.size()); + Iterator> it = s.iterator(); while (it.hasNext()) { - Map.Entry e = (Map.Entry) it.next(); + Map.Entry e = it.next(); assertTrue( (e.getKey().equals(one) && e.getValue().equals("A")) || (e.getKey().equals(two) && e.getValue().equals("B")) || @@ -390,22 +462,22 @@ public void testEntrySet() { * putAll adds all key-value pairs from the given map */ public void testPutAll() { - ConcurrentMap empty = map(); - ConcurrentMap map = map5(); - empty.putAll(map); - assertEquals(5, empty.size()); - assertTrue(empty.containsKey(one)); - assertTrue(empty.containsKey(two)); - assertTrue(empty.containsKey(three)); - assertTrue(empty.containsKey(four)); - assertTrue(empty.containsKey(five)); + ConcurrentMap p = bounded(); + ConcurrentMap map = map5(); + p.putAll(map); + mustEqual(5, p.size()); + assertTrue(p.containsKey(one)); + assertTrue(p.containsKey(two)); + assertTrue(p.containsKey(three)); + assertTrue(p.containsKey(four)); + assertTrue(p.containsKey(five)); } /** * putIfAbsent works when the given key is not present */ public void testPutIfAbsent() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.putIfAbsent(six, "Z"); assertTrue(map.containsKey(six)); } @@ -414,15 +486,15 @@ public void testPutIfAbsent() { * putIfAbsent does not add the pair if the key is already present */ public void testPutIfAbsent2() { - ConcurrentMap map = map5(); - assertEquals("A", map.putIfAbsent(one, "Z")); + ConcurrentMap map = map5(); + mustEqual("A", map.putIfAbsent(one, "Z")); } /** * replace fails when the given key is not present */ public void testReplace() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); assertNull(map.replace(six, "Z")); assertFalse(map.containsKey(six)); } @@ -431,38 +503,38 @@ public void testReplace() { * replace succeeds if the key is already present */ public void testReplace2() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); assertNotNull(map.replace(one, "Z")); - assertEquals("Z", map.get(one)); + mustEqual("Z", map.get(one)); } /** * replace value fails when the given key not mapped to expected value */ public void testReplaceValue() { - ConcurrentMap map = map5(); - assertEquals("A", map.get(one)); + ConcurrentMap map = map5(); + mustEqual("A", map.get(one)); assertFalse(map.replace(one, "Z", "Z")); - assertEquals("A", map.get(one)); + mustEqual("A", map.get(one)); } /** * replace value succeeds when the given key mapped to expected value */ public void testReplaceValue2() { - ConcurrentMap map = map5(); - assertEquals("A", map.get(one)); + ConcurrentMap map = map5(); + mustEqual("A", map.get(one)); assertTrue(map.replace(one, "A", "Z")); - assertEquals("Z", map.get(one)); + mustEqual("Z", map.get(one)); } /** * remove removes the correct key-value pair from the map */ public void testRemove() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.remove(five); - assertEquals(4, map.size()); + mustEqual(4, map.size()); assertFalse(map.containsKey(five)); } @@ -470,12 +542,12 @@ public void testRemove() { * remove(key,value) removes only if pair present */ public void testRemove2() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); map.remove(five, "E"); - assertEquals(4, map.size()); + mustEqual(4, map.size()); assertFalse(map.containsKey(five)); map.remove(four, "A"); - assertEquals(4, map.size()); + mustEqual(4, map.size()); assertTrue(map.containsKey(four)); } @@ -483,17 +555,17 @@ public void testRemove2() { * size returns the correct values */ public void testSize() { - ConcurrentMap map = map5(); - ConcurrentMap empty = map(); - assertEquals(0, empty.size()); - assertEquals(5, map.size()); + ConcurrentMap map = map5(); + ConcurrentMap empty = bounded(); + mustEqual(0, empty.size()); + mustEqual(5, map.size()); } /** * toString contains toString of elements */ public void testToString() { - ConcurrentMap map = map5(); + ConcurrentMap map = map5(); String s = map.toString(); for (int i = 1; i <= 5; ++i) { assertTrue(s.contains(String.valueOf(i))); @@ -502,11 +574,81 @@ public void testToString() { // Exception tests +// /** +// * Cannot create with only negative capacity +// */ +// public void testConstructor1() { +// try { +// new ConcurrentHashMap(-1); +// shouldThrow(); +// } catch (IllegalArgumentException success) {} +// } +// +// /** +// * Constructor (initialCapacity, loadFactor) throws +// * IllegalArgumentException if either argument is negative +// */ +// public void testConstructor2() { +// try { +// new ConcurrentHashMap(-1, .75f); +// shouldThrow(); +// } catch (IllegalArgumentException success) {} +// +// try { +// new ConcurrentHashMap(16, -1); +// shouldThrow(); +// } catch (IllegalArgumentException success) {} +// } +// +// /** +// * Constructor (initialCapacity, loadFactor, concurrencyLevel) +// * throws IllegalArgumentException if any argument is negative +// */ +// public void testConstructor3() { +// try { +// new ConcurrentHashMap(-1, .75f, 1); +// shouldThrow(); +// } catch (IllegalArgumentException success) {} +// +// try { +// new ConcurrentHashMap(16, -1, 1); +// shouldThrow(); +// } catch (IllegalArgumentException success) {} +// +// try { +// new ConcurrentHashMap(16, .75f, -1); +// shouldThrow(); +// } catch (IllegalArgumentException success) {} +// } +// +// /** +// * ConcurrentHashMap(map) throws NullPointerException if the given +// * map is null +// */ +// public void testConstructor4() { +// try { +// new ConcurrentHashMap(null); +// shouldThrow(); +// } catch (NullPointerException success) {} +// } +// +// /** +// * ConcurrentHashMap(map) creates a new map with the same mappings +// * as the given map +// */ +// public void testConstructor5() { +// ConcurrentHashMap map1 = map5(); +// ConcurrentHashMap map2 = map(map1); +// assertTrue(map2.equals(map1)); +// map2.put(one, "F"); +// assertFalse(map2.equals(map1)); +// } + /** * get(null) throws NPE */ public void testGet_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { c.get(null); shouldThrow(); @@ -517,7 +659,7 @@ public void testGet_NullPointerException() { * containsKey(null) throws NPE */ public void testContainsKey_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { c.containsKey(null); shouldThrow(); @@ -528,18 +670,29 @@ public void testContainsKey_NullPointerException() { * containsValue(null) throws NPE */ public void testContainsValue_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { c.containsValue(null); shouldThrow(); } catch (NullPointerException success) {} } +// /** +// * contains(null) throws NPE +// */ +// public void testContains_NullPointerException() { +// ConcurrentMap c = map(); +// try { +// c.contains(null); +// shouldThrow(); +// } catch (NullPointerException success) {} +// } + /** * put(null,x) throws NPE */ public void testPut1_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { c.put(null, "whatever"); shouldThrow(); @@ -550,9 +703,9 @@ public void testPut1_NullPointerException() { * put(x, null) throws NPE */ public void testPut2_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { - c.put("whatever", null); + c.put(zero, null); shouldThrow(); } catch (NullPointerException success) {} } @@ -561,7 +714,7 @@ public void testPut2_NullPointerException() { * putIfAbsent(null, x) throws NPE */ public void testPutIfAbsent1_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { c.putIfAbsent(null, "whatever"); shouldThrow(); @@ -572,7 +725,7 @@ public void testPutIfAbsent1_NullPointerException() { * replace(null, x) throws NPE */ public void testReplace_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { c.replace(null, "whatever"); shouldThrow(); @@ -583,9 +736,9 @@ public void testReplace_NullPointerException() { * replace(null, x, y) throws NPE */ public void testReplaceValue_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { - c.replace(null, one, "whatever"); + c.replace(null, "A", "B"); shouldThrow(); } catch (NullPointerException success) {} } @@ -594,9 +747,9 @@ public void testReplaceValue_NullPointerException() { * putIfAbsent(x, null) throws NPE */ public void testPutIfAbsent2_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { - c.putIfAbsent("whatever", null); + c.putIfAbsent(zero, null); shouldThrow(); } catch (NullPointerException success) {} } @@ -605,9 +758,9 @@ public void testPutIfAbsent2_NullPointerException() { * replace(x, null) throws NPE */ public void testReplace2_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { - c.replace("whatever", null); + c.replace(one, null); shouldThrow(); } catch (NullPointerException success) {} } @@ -616,9 +769,9 @@ public void testReplace2_NullPointerException() { * replace(x, null, y) throws NPE */ public void testReplaceValue2_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { - c.replace("whatever", null, "A"); + c.replace(one, null, "A"); shouldThrow(); } catch (NullPointerException success) {} } @@ -627,9 +780,9 @@ public void testReplaceValue2_NullPointerException() { * replace(x, y, null) throws NPE */ public void testReplaceValue3_NullPointerException() { - ConcurrentMap c = map(); + ConcurrentMap c = bounded(); try { - c.replace("whatever", one, null); + c.replace(zero, "A", null); shouldThrow(); } catch (NullPointerException success) {} } @@ -638,8 +791,8 @@ public void testReplaceValue3_NullPointerException() { * remove(null) throws NPE */ public void testRemove1_NullPointerException() { - ConcurrentMap c = map(); - c.put("sadsdf", "asdads"); + ConcurrentMap c = bounded(); + c.put(one, "asdads"); try { c.remove(null); shouldThrow(); @@ -650,8 +803,8 @@ public void testRemove1_NullPointerException() { * remove(null, x) throws NPE */ public void testRemove2_NullPointerException() { - ConcurrentMap c = map(); - c.put("sadsdf", "asdads"); + ConcurrentMap c = bounded(); + c.put(one, "asdads"); try { c.remove(null, "whatever"); shouldThrow(); @@ -662,32 +815,86 @@ public void testRemove2_NullPointerException() { * remove(x, null) returns false */ public void testRemove3() { - ConcurrentMap c = map(); - c.put("sadsdf", "asdads"); - assertFalse(c.remove("sadsdf", null)); + ConcurrentMap c = bounded(); + c.put(one, "asdads"); + assertFalse(c.remove(one, null)); } +// /** +// * A deserialized/reserialized map equals original +// */ +// public void testSerialization() throws Exception { +// Map x = map5(); +// Map y = serialClone(x); +// +// assertNotSame(x, y); +// mustEqual(x.size(), y.size()); +// mustEqual(x, y); +// mustEqual(y, x); +// } + /** * SetValue of an EntrySet entry sets value in the map. */ + @SuppressWarnings("unchecked") public void testSetValueWriteThrough() { // Adapted from a bug report by Eric Zoerner - ConcurrentMap map = map(); + ConcurrentMap map = bounded(); assertTrue(map.isEmpty()); for (int i = 0; i < 20; i++) { - map.put(i, i); + map.put(itemFor(i), itemFor(i)); } assertFalse(map.isEmpty()); - Map.Entry entry1 = (Map.Entry)map.entrySet().iterator().next(); + Item key = itemFor(16); + Map.Entry entry1 = map.entrySet().iterator().next(); // Unless it happens to be first (in which case remainder of // test is skipped), remove a possibly-colliding key from map // which, under some implementations, may cause entry1 to be // cloned in map - if (!entry1.getKey().equals(16)) { - map.remove(16); + if (!entry1.getKey().equals(key)) { + map.remove(key); entry1.setValue("XYZ"); assertTrue(map.containsValue("XYZ")); // fails if write-through broken } } + /** + * Tests performance of removeAll when the other collection is much smaller. + * ant -Djsr166.tckTestClass=ConcurrentHashMapTest -Djsr166.methodFilter=testRemoveAll_performance -Djsr166.expensiveTests=true tck + */ + public void testRemoveAll_performance() { + final int mapSize = expensiveTests ? 1_000_000 : 100; + final int iterations = expensiveTests ? 500 : 2; + final ConcurrentMap map = bounded(); + for (int i = 0; i < mapSize; i++) { + Item I = itemFor(i); + map.put(I, I); + } + Set keySet = map.keySet(); + Collection removeMe = Arrays.asList(new Item[] { minusOne, minusTwo }); + for (int i = 0; i < iterations; i++) { + assertFalse(keySet.removeAll(removeMe)); + } + mustEqual(mapSize, map.size()); + } + + public void testReentrantComputeIfAbsent() { + if (Runtime.version().feature() < 14) { + return; + } + + ConcurrentMap map = bounded(); + try { + for (int i = 0; i < 100; i++) { // force a resize + map.computeIfAbsent(new Item(i), key -> new Item(findValue(map, key))); + } + fail("recursive computeIfAbsent should throw IllegalStateException"); + } catch (IllegalStateException success) {} + } + + private static Item findValue(ConcurrentMap map, + Item key) { + return (key.value % 5 == 0) ? key : + map.computeIfAbsent(new Item(key.value + 1), k -> new Item(findValue(map, k))); + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Item.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Item.java new file mode 100644 index 0000000000..244f7b8022 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/Item.java @@ -0,0 +1,64 @@ +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * A simple element class for collections etc + */ +@SuppressWarnings({"serial", "NonOverridingEquals"}) +public final class Item extends Number implements Comparable, Serializable { + public final int value; + public Item(int v) { value = v; } + public Item(Item i) { value = i.value; } + public Item(Integer i) { value = i.intValue(); } + public static Item valueOf(int i) { return new Item(i); } + + @Override + public int intValue() { return value; } + @Override + public long longValue() { return value; } + @Override + public float floatValue() { return value; } + @Override + public double doubleValue() { return value; } + + @Override + public boolean equals(Object x) { + return (x instanceof Item) && ((Item)x).value == value; + } + public boolean equals(int b) { + return value == b; + } + @Override + public int compareTo(Item x) { + return Integer.compare(this.value, x.value); + } + public int compareTo(int b) { + return Integer.compare(this.value, b); + } + + @Override + public int hashCode() { return value; } + @Override + public String toString() { return Integer.toString(value); } + public static int compare(Item x, Item y) { + return Integer.compare(x.value, y.value); + } + public static int compare(Item x, int b) { + return Integer.compare(x.value, b); + } + + public static Comparator comparator() { return new Cpr(); } + public static class Cpr implements Comparator { + @Override + public int compare(Item x, Item y) { + return Integer.compare(x.value, y.value); + } + } +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/JSR166TestCase.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/JSR166TestCase.java index 87cb3ea9c1..a2f40b9566 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/JSR166TestCase.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/JSR166TestCase.java @@ -1,46 +1,104 @@ /* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ * Other contributors include Andrew Wright, Jeffrey Hayes, * Pat Fisher, Mike Judd. */ + +/* + * @test + * @summary JSR-166 tck tests, in a number of variations. + * The first is the conformance testing variant, + * while others also test implementation details. + * @build * + * @modules java.management + * @run junit/othervm/timeout=1000 JSR166TestCase + * @run junit/othervm/timeout=1000 + * --add-opens java.base/java.util.concurrent=ALL-UNNAMED + * --add-opens java.base/java.lang=ALL-UNNAMED + * -Djsr166.testImplementationDetails=true + * JSR166TestCase + * @run junit/othervm/timeout=1000 + * --add-opens java.base/java.util.concurrent=ALL-UNNAMED + * --add-opens java.base/java.lang=ALL-UNNAMED + * -Djsr166.testImplementationDetails=true + * -Djava.util.concurrent.ForkJoinPool.common.parallelism=0 + * JSR166TestCase + * @run junit/othervm/timeout=1000 + * --add-opens java.base/java.util.concurrent=ALL-UNNAMED + * --add-opens java.base/java.lang=ALL-UNNAMED + * -Djsr166.testImplementationDetails=true + * -Djava.util.concurrent.ForkJoinPool.common.parallelism=1 + * -Djava.util.secureRandomSeed=true + * JSR166TestCase + * @run junit/othervm/timeout=1000/policy=tck.policy + * --add-opens java.base/java.util.concurrent=ALL-UNNAMED + * --add-opens java.base/java.lang=ALL-UNNAMED + * -Djsr166.testImplementationDetails=true + * JSR166TestCase + */ package com.github.benmanes.caffeine.jsr166; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.NANOSECONDS; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.lang.management.LockInfo; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.Deque; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import java.util.concurrent.RecursiveAction; import java.util.concurrent.RecursiveTask; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; -import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestCase; +import junit.framework.TestResult; import junit.framework.TestSuite; /** @@ -53,18 +111,26 @@ * *
    * - *
  1. All assertions in code running in generated threads must use - * the forms {@link #threadFail}, {@link #threadAssertTrue}, {@link - * #threadAssertEquals}, or {@link #threadAssertNull}, (not - * {@code fail}, {@code assertTrue}, etc.) It is OK (but not - * particularly recommended) for other code to use these forms too. - * Only the most typically used JUnit assertion methods are defined - * this way, but enough to live with.
  2. + *
  3. All code not running in the main test thread (manually spawned threads + * or the common fork join pool) must be checked for failure (and completion!). + * Mechanisms that can be used to ensure this are: + *
      + *
    1. Signalling via a synchronizer like AtomicInteger or CountDownLatch + * that the task completed normally, which is checked before returning from + * the test method in the main thread. + *
    2. Using the forms {@link #threadFail}, {@link #threadAssertTrue}, + * or {@link #threadAssertNull}, (not {@code fail}, {@code assertTrue}, etc.) + * Only the most typically used JUnit assertion methods are defined + * this way, but enough to live with. + *
    3. Recording failure explicitly using {@link #threadUnexpectedException} + * or {@link #threadRecordFailure}. + *
    4. Using a wrapper like CheckedRunnable that uses one the mechanisms above. + *
    * - *
  4. If you override {@link #setUp} or {@link #tearDown}, make sure + *
  5. If you override {@link #setUp} or {@link #tearDown}, make sure * to invoke {@code super.setUp} and {@code super.tearDown} within * them. These methods are used to clear and check for thread - * assertion failures.
  6. + * assertion failures. * *
  7. All delays and timeouts must use one of the constants {@code * SHORT_DELAY_MS}, {@code SMALL_DELAY_MS}, {@code MEDIUM_DELAY_MS}, @@ -75,50 +141,60 @@ * is always discriminable as larger than SHORT and smaller than * MEDIUM. And so on. These constants are set to conservative values, * but even so, if there is ever any doubt, they can all be increased - * in one spot to rerun tests on slower platforms.
  8. + * in one spot to rerun tests on slower platforms. + * + * Class Item is used for elements of collections and related + * purposes. Many tests rely on their keys being equal to ints. To + * check these, methods mustEqual, mustContain, etc adapt the JUnit + * assert methods to intercept ints. * - *
  9. All threads generated must be joined inside each test case + *
  10. All threads generated must be joined inside each test case * method (or {@code fail} to do so) before returning from the * method. The {@code joinPool} method can be used to do this when - * using Executors.
  11. + * using Executors. * *
* *

Other notes *

    * - *
  • Usually, there is one testcase method per JSR166 method + *
  • Usually, there is one testcase method per JSR166 method * covering "normal" operation, and then as many exception-testing * methods as there are exceptions the method can throw. Sometimes * there are multiple tests per JSR166 method when the different * "normal" behaviors differ significantly. And sometimes testcases - * cover multiple methods when they cannot be tested in - * isolation.
  • + * cover multiple methods when they cannot be tested in isolation. * - *
  • The documentation style for testcases is to provide as javadoc + *
  • The documentation style for testcases is to provide as javadoc * a simple sentence or two describing the property that the testcase * method purports to test. The javadocs do not say anything about how - * the property is tested. To find out, read the code.
  • + * the property is tested. To find out, read the code. * - *
  • These tests are "conformance tests", and do not attempt to + *
  • These tests are "conformance tests", and do not attempt to * test throughput, latency, scalability or other performance factors * (see the separate "jtreg" tests for a set intended to check these * for the most central aspects of functionality.) So, most tests use * the smallest sensible numbers of threads, collection sizes, etc - * needed to check basic conformance.
  • + * needed to check basic conformance. * *
  • The test classes currently do not declare inclusion in * any particular package to simplify things for people integrating - * them in TCK test suites.
  • + * them in TCK test suites. * - *
  • As a convenience, the {@code main} of this class (JSR166TestCase) - * runs all JSR166 unit tests.
  • + *
  • As a convenience, the {@code main} of this class (JSR166TestCase) + * runs all JSR166 unit tests. * *
*/ -@SuppressWarnings({"deprecation", "rawtypes", "serial", "AssertionFailureIgnored", - "DeprecatedThreadMethods", "JdkObsolete", "JavaUtilDate", "ThreadPriorityCheck"}) +@SuppressWarnings({"rawtypes", "serial", "try", "unchecked", "AnnotateFormatMethod", + "EqualsIncompatibleType", "FunctionalInterfaceClash", "JavaUtilDate", + "JUnit3FloatingPointComparisonWithoutDelta", "NumericEquality", "ReferenceEquality", + "RethrowReflectiveOperationExceptionAsLinkageError", "SwitchDefault", "ThreadPriorityCheck", + "UndefinedEquals"}) public class JSR166TestCase extends TestCase { +// private static final boolean useSecurityManager = +// Boolean.getBoolean("jsr166.useSecurityManager"); + protected static final boolean expensiveTests = Boolean.getBoolean("jsr166.expensiveTests"); @@ -149,6 +225,61 @@ public class JSR166TestCase extends TestCase { private static final int runsPerTest = Integer.getInteger("jsr166.runsPerTest", 1); + /** + * The number of repetitions of the test suite (for finding leaks?). + */ + private static final int suiteRuns = + Integer.getInteger("jsr166.suiteRuns", 1); + + /** + * Returns the value of the system property, or NaN if not defined. + */ + private static float systemPropertyValue(String name) { + String floatString = System.getProperty(name); + if (floatString == null) { + return Float.NaN; + } + try { + return Float.parseFloat(floatString); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException( + String.format("Bad float value in system property %s=%s", + name, floatString)); + } + } + + private static final ThreadMXBean THREAD_MXBEAN + = ManagementFactory.getThreadMXBean(); + + /** + * The scaling factor to apply to standard delays used in tests. + * May be initialized from any of: + * - the "jsr166.delay.factor" system property + * - the "test.timeout.factor" system property (as used by jtreg) + * See: http://openjdk.java.net/jtreg/tag-spec.html + * - hard-coded fuzz factor when using a known slowpoke VM + */ + private static final float delayFactor = delayFactor(); + + private static float delayFactor() { + float x; + if (!Float.isNaN(x = systemPropertyValue("jsr166.delay.factor"))) { + return x; + } + if (!Float.isNaN(x = systemPropertyValue("test.timeout.factor"))) { + return x; + } + String prop = System.getProperty("java.vm.version"); + if (prop != null && prop.matches(".*debug.*")) + { + return 4.0f; // How much slower is fastdebug than product?! + } + return 1.0f; + } + + public JSR166TestCase() { super(); } + public JSR166TestCase(String name) { super(name); } + /** * A filter for tests to run, matching strings of the form * methodName(className), e.g. "testInvokeAll5(ForkJoinPoolTest)" @@ -161,49 +292,147 @@ private static Pattern methodFilter() { return (regex == null) ? null : Pattern.compile(regex); } + // Instrumentation to debug very rare, but very annoying hung test runs. + static volatile TestCase currentTestCase; + // static volatile int currentRun = 0; + static { + Runnable wedgedTestDetector = new Runnable() { @Override + public void run() { + // Avoid spurious reports with enormous runsPerTest. + // A single test case run should never take more than 1 second. + // But let's cap it at the high end too ... + final int timeoutMinutesMin = Math.max(runsPerTest / 60, 1) + * Math.max((int) delayFactor, 1); + final int timeoutMinutes = Math.min(15, timeoutMinutesMin); + for (TestCase lastTestCase = currentTestCase;;) { + try { MINUTES.sleep(timeoutMinutes); } + catch (InterruptedException unexpected) { break; } + if (lastTestCase == currentTestCase) { + System.err.printf( + "Looks like we're stuck running test: %s%n", + lastTestCase); +// System.err.printf( +// "Looks like we're stuck running test: %s (%d/%d)%n", +// lastTestCase, currentRun, runsPerTest); +// System.err.println("availableProcessors=" + +// Runtime.getRuntime().availableProcessors()); +// System.err.printf("cpu model = %s%n", cpuModel()); + dumpTestThreads(); + // one stack dump is probably enough; more would be spam + break; + } + lastTestCase = currentTestCase; + }}}; + Thread thread = new Thread(wedgedTestDetector, "WedgedTestDetector"); + thread.setDaemon(true); + thread.start(); + } + +// public static String cpuModel() { +// try { +// java.util.regex.Matcher matcher +// = Pattern.compile("model name\\s*: (.*)") +// .matcher(new String( +// java.nio.file.Files.readAllBytes( +// java.nio.file.Paths.get("/proc/cpuinfo")), "UTF-8")); +// matcher.find(); +// return matcher.group(1); +// } catch (Exception ex) { return null; } +// } + @Override - protected void runTest() throws Throwable { + public void runBare() throws Throwable { + currentTestCase = this; if (methodFilter == null || methodFilter.matcher(toString()).find()) { - for (int i = 0; i < runsPerTest; i++) { - if (profileTests) { - runTestProfiled(); - } else { - super.runTest(); - } + super.runBare(); + } + } + + @Override + protected void runTest() throws Throwable { + for (int i = 0; i < runsPerTest; i++) { + // currentRun = i; + if (profileTests) { + runTestProfiled(); + } else { + super.runTest(); } } } protected void runTestProfiled() throws Throwable { - // Warmup run, notably to trigger all needed classloading. - super.runTest(); - long t0 = System.nanoTime(); - try { + for (int i = 0; i < 2; i++) { + long startTime = System.nanoTime(); super.runTest(); - } finally { - long elapsedMillis = millisElapsedSince(t0); - if (elapsedMillis >= profileThreshold) { - System.out.printf("%n%s: %d%n", toString(), elapsedMillis); + long elapsedMillis = millisElapsedSince(startTime); + if (elapsedMillis < profileThreshold) { + break; + } + // Never report first run of any test; treat it as a + // warmup run, notably to trigger all needed classloading, + if (i > 0) { + System.out.printf("%s: %d%n", toString(), elapsedMillis); } } } /** * Runs all JSR166 unit tests using junit.textui.TestRunner. - * Optional command line arg provides the number of iterations to - * repeat running the tests. */ public static void main(String[] args) { - int iters = (args.length == 0) ? 1 : Integer.parseInt(args[0]); + main(suite(), args); + } + + static class PithyResultPrinter extends junit.textui.ResultPrinter { + PithyResultPrinter(java.io.PrintStream writer) { super(writer); } + long runTime; + @Override + public void startTest(Test test) {} + @Override + protected void printHeader(long runTime) { + this.runTime = runTime; // defer printing for later + } + @Override + protected void printFooter(TestResult result) { + if (result.wasSuccessful()) { + getWriter().println("OK (" + result.runCount() + " tests)" + + " Time: " + elapsedTimeAsString(runTime)); + } else { + getWriter().println("Time: " + elapsedTimeAsString(runTime)); + super.printFooter(result); + } + } + } + + /** + * Returns a TestRunner that doesn't bother with unnecessary + * fluff, like printing a "." for each test case. + */ + static junit.textui.TestRunner newPithyTestRunner() { + junit.textui.TestRunner runner = new junit.textui.TestRunner(); + runner.setPrinter(new PithyResultPrinter(System.out)); + return runner; + } - Test s = suite(); - for (int i = 0; i < iters; ++i) { - junit.textui.TestRunner.run(s); + /** + * Runs all unit tests in the given test suite. + * Actual behavior influenced by jsr166.* system properties. + */ + static void main(Test suite, String[] args) { +// if (useSecurityManager) { +// System.err.println("Setting a permissive security manager"); +// Policy.setPolicy(permissivePolicy()); +// System.setSecurityManager(new SecurityManager()); +// } + for (int i = 0; i < suiteRuns; i++) { + TestResult result = newPithyTestRunner().doRun(suite); + if (!result.wasSuccessful()) { + System.exit(1); + } System.gc(); System.runFinalization(); } - System.exit(0); } public static TestSuite newTestSuite(Object... suiteOrClasses) { @@ -225,25 +454,232 @@ public static void addNamedTestClasses(TestSuite suite, for (String testClassName : testClassNames) { try { Class testClass = Class.forName(testClassName); - Method m = testClass.getDeclaredMethod("suite", - new Class[0]); + Method m = testClass.getDeclaredMethod("suite"); suite.addTest(newTestSuite((Test)m.invoke(null))); - } catch (Exception e) { - throw new Error("Missing test class", e); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Missing test class", e); } } } + public static final double JAVA_CLASS_VERSION; + public static final String JAVA_SPECIFICATION_VERSION; + static { + try { + JAVA_CLASS_VERSION = java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + @Override + public Double run() { + return Double.valueOf(System.getProperty("java.class.version"));}}); + JAVA_SPECIFICATION_VERSION = java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + @Override + public String run() { + return System.getProperty("java.specification.version");}}); + } catch (Throwable t) { + throw new Error(t); + } + } + + public static boolean atLeastJava6() { return JAVA_CLASS_VERSION >= 50.0; } + public static boolean atLeastJava7() { return JAVA_CLASS_VERSION >= 51.0; } + public static boolean atLeastJava8() { return JAVA_CLASS_VERSION >= 52.0; } + public static boolean atLeastJava9() { return JAVA_CLASS_VERSION >= 53.0; } + public static boolean atLeastJava10() { return JAVA_CLASS_VERSION >= 54.0; } + public static boolean atLeastJava11() { return JAVA_CLASS_VERSION >= 55.0; } + public static boolean atLeastJava12() { return JAVA_CLASS_VERSION >= 56.0; } + public static boolean atLeastJava13() { return JAVA_CLASS_VERSION >= 57.0; } + public static boolean atLeastJava14() { return JAVA_CLASS_VERSION >= 58.0; } + public static boolean atLeastJava15() { return JAVA_CLASS_VERSION >= 59.0; } + public static boolean atLeastJava16() { return JAVA_CLASS_VERSION >= 60.0; } + public static boolean atLeastJava17() { return JAVA_CLASS_VERSION >= 61.0; } + /** * Collects all JSR166 unit tests as one suite. */ public static Test suite() { + // Java7+ test classes TestSuite suite = newTestSuite( - ConcurrentHashMapTest.suite()); - addNamedTestClasses(suite, ConcurrentHashMap8Test.class.getName()); +// ForkJoinPoolTest.suite(), +// ForkJoinTaskTest.suite(), +// RecursiveActionTest.suite(), +// RecursiveTaskTest.suite(), +// LinkedTransferQueueTest.suite(), +// PhaserTest.suite(), +// ThreadLocalRandomTest.suite(), +// AbstractExecutorServiceTest.suite(), +// AbstractQueueTest.suite(), +// AbstractQueuedSynchronizerTest.suite(), +// AbstractQueuedLongSynchronizerTest.suite(), +// ArrayBlockingQueueTest.suite(), +// ArrayDequeTest.suite(), +// ArrayListTest.suite(), +// AtomicBooleanTest.suite(), +// AtomicIntegerArrayTest.suite(), +// AtomicIntegerFieldUpdaterTest.suite(), +// AtomicIntegerTest.suite(), +// AtomicLongArrayTest.suite(), +// AtomicLongFieldUpdaterTest.suite(), +// AtomicLongTest.suite(), +// AtomicMarkableReferenceTest.suite(), +// AtomicReferenceArrayTest.suite(), +// AtomicReferenceFieldUpdaterTest.suite(), +// AtomicReferenceTest.suite(), +// AtomicStampedReferenceTest.suite(), + ConcurrentHashMapTest.suite() +// ConcurrentLinkedDequeTest.suite(), +// ConcurrentLinkedQueueTest.suite(), +// ConcurrentSkipListMapTest.suite(), +// ConcurrentSkipListSubMapTest.suite(), +// ConcurrentSkipListSetTest.suite(), +// ConcurrentSkipListSubSetTest.suite(), +// CopyOnWriteArrayListTest.suite(), +// CopyOnWriteArraySetTest.suite(), +// CountDownLatchTest.suite(), +// CountedCompleterTest.suite(), +// CyclicBarrierTest.suite(), +// DelayQueueTest.suite(), +// EntryTest.suite(), +// ExchangerTest.suite(), +// ExecutorsTest.suite(), +// ExecutorCompletionServiceTest.suite(), +// FutureTaskTest.suite(), +// HashtableTest.suite(), +// LinkedBlockingDequeTest.suite(), +// LinkedBlockingQueueTest.suite(), +// LinkedListTest.suite(), +// LockSupportTest.suite(), +// PriorityBlockingQueueTest.suite(), +// PriorityQueueTest.suite(), +// ReentrantLockTest.suite(), +// ReentrantReadWriteLockTest.suite(), +// ScheduledExecutorTest.suite(), +// ScheduledExecutorSubclassTest.suite(), +// SemaphoreTest.suite(), +// SynchronousQueueTest.suite(), +// SystemTest.suite(), +// ThreadLocalTest.suite(), +// ThreadPoolExecutorTest.suite(), +// ThreadPoolExecutorSubclassTest.suite(), +// ThreadTest.suite(), +// TimeUnitTest.suite(), +// TreeMapTest.suite(), +// TreeSetTest.suite(), +// TreeSubMapTest.suite(), +// TreeSubSetTest.suite(), +// VectorTest.suite() + ); + + // Java8+ test classes + if (atLeastJava8()) { + String[] java8TestClassNames = { +// "ArrayDeque8Test", +// "Atomic8Test", +// "CompletableFutureTest", + "com.github.benmanes.caffeine.jsr166.ConcurrentHashMap8Test", +// "CountedCompleter8Test", +// "DoubleAccumulatorTest", +// "DoubleAdderTest", +// "ForkJoinPool8Test", +// "ForkJoinTask8Test", +// "HashMapTest", +// "LinkedBlockingDeque8Test", +// "LinkedBlockingQueue8Test", +// "LinkedHashMapTest", +// "LongAccumulatorTest", +// "LongAdderTest", +// "SplittableRandomTest", +// "StampedLockTest", +// "SubmissionPublisherTest", +// "ThreadLocalRandom8Test", +// "TimeUnit8Test", + }; + addNamedTestClasses(suite, java8TestClassNames); + } + + // Java9+ test classes + if (atLeastJava9()) { + String[] java9TestClassNames = { +// "AtomicBoolean9Test", +// "AtomicInteger9Test", +// "AtomicIntegerArray9Test", +// "AtomicLong9Test", +// "AtomicLongArray9Test", +// "AtomicReference9Test", +// "AtomicReferenceArray9Test", +// "ExecutorCompletionService9Test", +// "ForkJoinPool9Test", + }; + addNamedTestClasses(suite, java9TestClassNames); + } + return suite; } + /** Returns list of junit-style test method names in given class. */ + public static ArrayList testMethodNames(Class testClass) { + Method[] methods = testClass.getDeclaredMethods(); + ArrayList names = new ArrayList<>(methods.length); + for (Method method : methods) { + if (method.getName().startsWith("test") + && Modifier.isPublic(method.getModifiers()) + // method.getParameterCount() requires jdk8+ + && method.getParameterTypes().length == 0) { + names.add(method.getName()); + } + } + return names; + } + + /** + * Returns junit-style testSuite for the given test class, but + * parameterized by passing extra data to each test. + */ + public static Test parameterizedTestSuite + (Class testClass, + Class dataClass, + ExtraData data) { + try { + TestSuite suite = new TestSuite(); + Constructor c = + testClass.getDeclaredConstructor(dataClass, String.class); + for (String methodName : testMethodNames(testClass)) { + suite.addTest((Test) c.newInstance(data, methodName)); + } + return suite; + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + /** + * Returns junit-style testSuite for the jdk8 extension of the + * given test class, but parameterized by passing extra data to + * each test. Uses reflection to allow compilation in jdk7. + */ + public static Test jdk8ParameterizedTestSuite + (Class testClass, + Class dataClass, + ExtraData data) { + if (atLeastJava8()) { + String name = testClass.getName(); + String name8 = name.replaceAll("Test$", "8Test"); + if (name.equals(name8)) { + throw new AssertionError(name); + } + try { + return (Test) + Class.forName(name8) + .getMethod("testSuite", dataClass) + .invoke(null, data); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } else { + return new TestSuite(); + } + } + // Delays for timing-dependent tests, in milliseconds. public static long SHORT_DELAY_MS; @@ -252,11 +688,68 @@ public static Test suite() { public static long LONG_DELAY_MS; /** - * Returns the shortest timed delay. This could - * be reimplemented to use for example a Property. + * A delay significantly longer than LONG_DELAY_MS. + * Use this in a thread that is waited for via awaitTermination(Thread). + */ + public static long LONGER_DELAY_MS; + + private static final long RANDOM_TIMEOUT; + private static final long RANDOM_EXPIRED_TIMEOUT; + private static final TimeUnit RANDOM_TIMEUNIT; + static { + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + long[] timeouts = { Long.MIN_VALUE, -1, 0, 1, Long.MAX_VALUE }; + RANDOM_TIMEOUT = timeouts[rnd.nextInt(timeouts.length)]; + RANDOM_EXPIRED_TIMEOUT = timeouts[rnd.nextInt(3)]; + TimeUnit[] timeUnits = TimeUnit.values(); + RANDOM_TIMEUNIT = timeUnits[rnd.nextInt(timeUnits.length)]; + } + + /** + * Returns a timeout for use when any value at all will do. + */ + static long randomTimeout() { return RANDOM_TIMEOUT; } + + /** + * Returns a timeout that means "no waiting", i.e. not positive. + */ + static long randomExpiredTimeout() { return RANDOM_EXPIRED_TIMEOUT; } + + /** + * Returns a random non-null TimeUnit. + */ + static TimeUnit randomTimeUnit() { return RANDOM_TIMEUNIT; } + + /** + * Returns a random boolean; a "coin flip". + */ + static boolean randomBoolean() { + return ThreadLocalRandom.current().nextBoolean(); + } + + /** + * Returns a random element from given choices. + */ + T chooseRandomly(List choices) { + return choices.get(ThreadLocalRandom.current().nextInt(choices.size())); + } + + /** + * Returns a random element from given choices. + */ + @SuppressWarnings("unchecked") + T chooseRandomly(T... choices) { + return choices[ThreadLocalRandom.current().nextInt(choices.length)]; + } + + /** + * Returns the shortest timed delay. This can be scaled up for + * slow machines using the jsr166.delay.factor system property, + * or via jtreg's -timeoutFactor: flag. + * http://openjdk.java.net/jtreg/command-help.html */ protected long getShortDelay() { - return 50; + return (long) (50 * delayFactor); } /** @@ -267,29 +760,36 @@ protected void setDelays() { SMALL_DELAY_MS = SHORT_DELAY_MS * 5; MEDIUM_DELAY_MS = SHORT_DELAY_MS * 10; LONG_DELAY_MS = SHORT_DELAY_MS * 200; + LONGER_DELAY_MS = 2 * LONG_DELAY_MS; } + private static final long TIMEOUT_DELAY_MS + = (long) (12.0 * Math.cbrt(delayFactor)); + /** - * Returns a timeout in milliseconds to be used in tests that - * verify that operations block or time out. + * Returns a timeout in milliseconds to be used in tests that verify + * that operations block or time out. We want this to be longer + * than the OS scheduling quantum, but not too long, so don't scale + * linearly with delayFactor; we use "crazy" cube root instead. */ - long timeoutMillis() { - return SHORT_DELAY_MS / 4; + static long timeoutMillis() { + return TIMEOUT_DELAY_MS; } /** - * Returns a new Date instance representing a time delayMillis - * milliseconds in the future. + * Returns a new Date instance representing a time at least + * delayMillis milliseconds in the future. */ Date delayedDate(long delayMillis) { - return new Date(System.currentTimeMillis() + delayMillis); + // Add 1 because currentTimeMillis is known to round into the past. + return new Date(System.currentTimeMillis() + delayMillis + 1); } /** * The first exception encountered if any threadAssertXXX method fails. */ private final AtomicReference threadFailure - = new AtomicReference(null); + = new AtomicReference<>(null); /** * Records an exception so that it can be rethrown later in the test @@ -298,7 +798,10 @@ Date delayedDate(long delayMillis) { * the same test have no effect. */ public void threadRecordFailure(Throwable t) { - threadFailure.compareAndSet(null, t); + System.err.println(t); + if (threadFailure.compareAndSet(null, t)) { + dumpTestThreads(); + } } @Override @@ -306,6 +809,13 @@ public void setUp() { setDelays(); } + void tearDownFail(String format, Object... args) { + String msg = toString() + ": " + String.format(format, args); + System.err.println(msg); + dumpTestThreads(); + throw new AssertionError(msg); + } + /** * Extra checks that get done for all test cases. * @@ -326,25 +836,22 @@ public void tearDown() throws Exception { } else if (t instanceof Exception) { throw (Exception) t; } else { - AssertionFailedError afe = - new AssertionFailedError(t.toString()); - afe.initCause(t); - throw afe; + throw new AssertionError(t.toString(), t); } } if (Thread.interrupted()) { - throw new AssertionFailedError("interrupt status set in main thread"); + tearDownFail("interrupt status set in main thread"); } checkForkJoinPoolThreadLeaks(); } /** - * Finds missing try { ... } finally { joinPool(e); } + * Finds missing PoolCleaners */ void checkForkJoinPoolThreadLeaks() throws InterruptedException { - Thread[] survivors = new Thread[5]; + Thread[] survivors = new Thread[7]; int count = Thread.enumerate(survivors); for (int i = 0; i < count; i++) { Thread thread = survivors[i]; @@ -352,97 +859,98 @@ void checkForkJoinPoolThreadLeaks() throws InterruptedException { if (name.startsWith("ForkJoinPool-")) { // give thread some time to terminate thread.join(LONG_DELAY_MS); - if (!thread.isAlive()) { - continue; + if (thread.isAlive()) { + tearDownFail("Found leaked ForkJoinPool thread thread=%s", + thread); } - thread.stop(); - throw new AssertionFailedError - (String.format("Found leaked ForkJoinPool thread test=%s thread=%s%n", - toString(), name)); } } + + if (!ForkJoinPool.commonPool() + .awaitQuiescence(LONG_DELAY_MS, MILLISECONDS)) { + tearDownFail("ForkJoin common pool thread stuck"); + } } /** * Just like fail(reason), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ - @SuppressWarnings("CatchFail") public void threadFail(String reason) { try { fail(reason); - } catch (AssertionFailedError t) { - threadRecordFailure(t); - fail(reason); + } catch (AssertionError fail) { + threadRecordFailure(fail); + throw fail; } } /** * Just like assertTrue(b), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ public void threadAssertTrue(boolean b) { try { assertTrue(b); - } catch (AssertionFailedError t) { - threadRecordFailure(t); - throw t; + } catch (AssertionError fail) { + threadRecordFailure(fail); + throw fail; } } /** * Just like assertFalse(b), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ public void threadAssertFalse(boolean b) { try { assertFalse(b); - } catch (AssertionFailedError t) { - threadRecordFailure(t); - throw t; + } catch (AssertionError fail) { + threadRecordFailure(fail); + throw fail; } } /** * Just like assertNull(x), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ public void threadAssertNull(Object x) { try { assertNull(x); - } catch (AssertionFailedError t) { - threadRecordFailure(t); - throw t; + } catch (AssertionError fail) { + threadRecordFailure(fail); + throw fail; } } /** * Just like assertEquals(x, y), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ public void threadAssertEquals(long x, long y) { try { assertEquals(x, y); - } catch (AssertionFailedError t) { - threadRecordFailure(t); - throw t; + } catch (AssertionError fail) { + threadRecordFailure(fail); + throw fail; } } /** * Just like assertEquals(x, y), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ public void threadAssertEquals(Object x, Object y) { try { assertEquals(x, y); - } catch (AssertionFailedError fail) { + } catch (AssertionError fail) { threadRecordFailure(fail); throw fail; } catch (Throwable fail) { @@ -452,13 +960,13 @@ public void threadAssertEquals(Object x, Object y) { /** * Just like assertSame(x, y), but additionally recording (using - * threadRecordFailure) any AssertionFailedError thrown, so that - * the current testcase will fail. + * threadRecordFailure) any AssertionError thrown, so that the + * current testcase will fail. */ public void threadAssertSame(Object x, Object y) { try { assertSame(x, y); - } catch (AssertionFailedError fail) { + } catch (AssertionError fail) { threadRecordFailure(fail); throw fail; } @@ -480,8 +988,8 @@ public void threadShouldThrow(String exceptionName) { /** * Records the given exception using {@link #threadRecordFailure}, - * then rethrows the exception, wrapping it in an - * AssertionFailedError if necessary. + * then rethrows the exception, wrapping it in an AssertionError + * if necessary. */ public void threadUnexpectedException(Throwable t) { threadRecordFailure(t); @@ -491,123 +999,255 @@ public void threadUnexpectedException(Throwable t) { } else if (t instanceof Error) { throw (Error) t; } else { - AssertionFailedError afe = - new AssertionFailedError("unexpected exception: " + t); - afe.initCause(t); - throw afe; + throw new AssertionError("unexpected exception: " + t, t); } } /** * Delays, via Thread.sleep, for the given millisecond delay, but * if the sleep is shorter than specified, may re-sleep or yield - * until time elapses. + * until time elapses. Ensures that the given time, as measured + * by System.nanoTime(), has elapsed. */ static void delay(long millis) throws InterruptedException { - long startTime = System.nanoTime(); - long ns = millis * 1000 * 1000; - for (;;) { + long nanos = millis * (1000 * 1000); + final long wakeupTime = System.nanoTime() + nanos; + do { if (millis > 0L) { Thread.sleep(millis); } else { Thread.yield(); } - long d = ns - (System.nanoTime() - startTime); - if (d > 0L) { - millis = d / (1000 * 1000); - } else { - break; + nanos = wakeupTime - System.nanoTime(); + millis = nanos / (1000 * 1000); + } while (nanos >= 0L); + } + + /** + * Allows use of try-with-resources with per-test thread pools. + */ + class PoolCleaner implements AutoCloseable { + private final ExecutorService pool; + public PoolCleaner(ExecutorService pool) { this.pool = pool; } + @Override + public void close() { joinPool(pool); } + } + + /** + * An extension of PoolCleaner that has an action to release the pool. + */ + class PoolCleanerWithReleaser extends PoolCleaner { + private final Runnable releaser; + public PoolCleanerWithReleaser(ExecutorService pool, Runnable releaser) { + super(pool); + this.releaser = releaser; + } + @Override + public void close() { + try { + releaser.run(); + } finally { + super.close(); } } } + PoolCleaner cleaner(ExecutorService pool) { + return new PoolCleaner(pool); + } + + PoolCleaner cleaner(ExecutorService pool, Runnable releaser) { + return new PoolCleanerWithReleaser(pool, releaser); + } + + PoolCleaner cleaner(ExecutorService pool, CountDownLatch latch) { + return new PoolCleanerWithReleaser(pool, releaser(latch)); + } + + Runnable releaser(final CountDownLatch latch) { + return new Runnable() { @Override + public void run() { + do { latch.countDown(); } + while (latch.getCount() > 0); + }}; + } + + PoolCleaner cleaner(ExecutorService pool, AtomicBoolean flag) { + return new PoolCleanerWithReleaser(pool, releaser(flag)); + } + + Runnable releaser(final AtomicBoolean flag) { + return new Runnable() { @Override + public void run() { flag.set(true); }}; + } + /** * Waits out termination of a thread pool or fails doing so. */ - @SuppressWarnings("CatchFail") - void joinPool(ExecutorService exec) { + void joinPool(ExecutorService pool) { try { - exec.shutdown(); - if (!exec.awaitTermination(2 * LONG_DELAY_MS, MILLISECONDS)) { - fail("ExecutorService " + exec + - " did not terminate in a timely manner"); + pool.shutdown(); + if (!pool.awaitTermination(2 * LONG_DELAY_MS, MILLISECONDS)) { + try { + threadFail("ExecutorService " + pool + + " did not terminate in a timely manner"); + } finally { + // last resort, for the benefit of subsequent tests + pool.shutdownNow(); + pool.awaitTermination(MEDIUM_DELAY_MS, MILLISECONDS); + } } } catch (SecurityException ok) { // Allowed in case test doesn't have privs } catch (InterruptedException fail) { - fail("Unexpected InterruptedException"); + threadFail("Unexpected InterruptedException"); } } /** - * A debugging tool to print all stack traces, as jstack does. + * Like Runnable, but with the freedom to throw anything. + * junit folks had the same idea: + * http://junit.org/junit5/docs/snapshot/api/org/junit/gen5/api/Executable.html */ - static void printAllStackTraces() { - for (ThreadInfo info : - ManagementFactory.getThreadMXBean() - .dumpAllThreads(true, true)) { - System.err.print(info); - } - } + interface Action { public void run() throws Throwable; } /** - * Checks that thread does not terminate within the default - * millisecond delay of {@code timeoutMillis()}. + * Runs all the given actions in parallel, failing if any fail. + * Useful for running multiple variants of tests that are + * necessarily individually slow because they must block. */ - void assertThreadStaysAlive(Thread thread) { - assertThreadStaysAlive(thread, timeoutMillis()); + void testInParallel(Action ... actions) { + ExecutorService pool = Executors.newCachedThreadPool(); + try (PoolCleaner cleaner = cleaner(pool)) { + ArrayList> futures = new ArrayList<>(actions.length); + for (final Action action : actions) { + futures.add(pool.submit(new CheckedRunnable() { + @Override + public void realRun() throws Throwable { action.run();}})); + } + for (Future future : futures) { + try { + assertNull(future.get(LONG_DELAY_MS, MILLISECONDS)); + } catch (ExecutionException ex) { + threadUnexpectedException(ex.getCause()); + } catch (Exception ex) { + threadUnexpectedException(ex); + } + } + } + } + + /** Returns true if thread info might be useful in a thread dump. */ + static boolean threadOfInterest(ThreadInfo info) { + final String name = info.getThreadName(); + String lockName; + if (name == null) { + return true; + } + if (name.equals("Signal Dispatcher") + || name.equals("WedgedTestDetector")) { + return false; + } + if (name.equals("Reference Handler")) { + // Reference Handler stacktrace changed in JDK-8156500 + StackTraceElement[] stackTrace; String methodName; + if ((stackTrace = info.getStackTrace()) != null + && stackTrace.length > 0 + && (methodName = stackTrace[0].getMethodName()) != null + && methodName.equals("waitForReferencePendingList")) { + return false; + } + // jdk8 Reference Handler stacktrace + if ((lockName = info.getLockName()) != null + && lockName.startsWith("java.lang.ref")) { + return false; + } + } + if ((name.equals("Finalizer") || name.equals("Common-Cleaner")) + && (lockName = info.getLockName()) != null + && lockName.startsWith("java.lang.ref")) { + return false; + } + if (name.startsWith("ForkJoinPool.commonPool-worker") + && (lockName = info.getLockName()) != null + && lockName.startsWith("java.util.concurrent.ForkJoinPool")) { + return false; + } + return true; } /** - * Checks that thread does not terminate within the given millisecond delay. + * A debugging tool to print stack traces of most threads, as jstack does. + * Uninteresting threads are filtered out. */ - @SuppressWarnings("CatchFail") - void assertThreadStaysAlive(Thread thread, long millis) { - try { - // No need to optimize the failing case via Thread.join. - delay(millis); - assertTrue(thread.isAlive()); - } catch (InterruptedException fail) { - fail("Unexpected InterruptedException"); + static void dumpTestThreads() { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + try { + System.setSecurityManager(null); + } catch (SecurityException giveUp) { + return; + } + } + + System.err.println("------ stacktrace dump start ------"); + for (ThreadInfo info : THREAD_MXBEAN.dumpAllThreads(true, true)) { + if (threadOfInterest(info)) { + System.err.print(info); + } + } + System.err.println("------ stacktrace dump end ------"); + + if (sm != null) { + System.setSecurityManager(sm); } } /** - * Checks that the threads do not terminate within the default - * millisecond delay of {@code timeoutMillis()}. + * Checks that thread eventually enters the expected blocked thread state. */ - void assertThreadsStayAlive(Thread... threads) { - assertThreadsStayAlive(timeoutMillis(), threads); + void assertThreadBlocks(Thread thread, Thread.State expected) { + // always sleep at least 1 ms, with high probability avoiding + // transitory states + for (long retries = LONG_DELAY_MS * 3 / 4; retries-->0; ) { + try { delay(1); } + catch (InterruptedException fail) { + throw new AssertionError("Unexpected InterruptedException", fail); + } + Thread.State s = thread.getState(); + if (s == expected) { + return; + } else if (s == Thread.State.TERMINATED) { + fail("Unexpected thread termination"); + } + } + fail("timed out waiting for thread to enter thread state " + expected); } /** - * Checks that the threads do not terminate within the given millisecond delay. + * Returns the thread's blocker's class name, if any, else null. */ - @SuppressWarnings("CatchFail") - void assertThreadsStayAlive(long millis, Thread... threads) { - try { - // No need to optimize the failing case via Thread.join. - delay(millis); - for (Thread thread : threads) { - assertTrue(thread.isAlive()); - } - } catch (InterruptedException fail) { - fail("Unexpected InterruptedException"); + String blockerClassName(Thread thread) { + ThreadInfo threadInfo; LockInfo lockInfo; + if ((threadInfo = THREAD_MXBEAN.getThreadInfo(thread.getId(), 0)) != null + && (lockInfo = threadInfo.getLockInfo()) != null) { + return lockInfo.getClassName(); } + return null; } /** * Checks that future.get times out, with the default timeout of * {@code timeoutMillis()}. */ - void assertFutureTimesOut(Future future) { + void assertFutureTimesOut(Future future) { assertFutureTimesOut(future, timeoutMillis()); } /** * Checks that future.get times out, with the given millisecond timeout. */ - void assertFutureTimesOut(Future future, long timeoutMillis) { + void assertFutureTimesOut(Future future, long timeoutMillis) { long startTime = System.nanoTime(); try { future.get(timeoutMillis, MILLISECONDS); @@ -615,8 +1255,9 @@ void assertFutureTimesOut(Future future, long timeoutMillis) { } catch (TimeoutException success) { } catch (Exception fail) { threadUnexpectedException(fail); - } finally { future.cancel(true); } + } assertTrue(millisElapsedSince(startTime) >= timeoutMillis); + assertFalse(future.isDone()); } /** @@ -633,74 +1274,354 @@ public void shouldThrow(String exceptionName) { fail("Should throw " + exceptionName); } + /** + * The maximum number of consecutive spurious wakeups we should + * tolerate (from APIs like LockSupport.park) before failing a test. + */ + static final int MAX_SPURIOUS_WAKEUPS = 10; + /** * The number of elements to place in collections, arrays, etc. + * Must be at least ten; */ - public static final int SIZE = 20; - - // Some convenient Integer constants - - public static final Integer zero = 0; - public static final Integer one = 1; - public static final Integer two = 2; - public static final Integer three = 3; - public static final Integer four = 4; - public static final Integer five = 5; - public static final Integer six = 6; - public static final Integer seven = 7; - public static final Integer eight = 8; - public static final Integer nine = 9; - public static final Integer m1 = -1; - public static final Integer m2 = -2; - public static final Integer m3 = -3; - public static final Integer m4 = -4; - public static final Integer m5 = -5; - public static final Integer m6 = -6; - public static final Integer m10 = -10; + public static final int SIZE = 32; + + static Item[] seqItems(int size) { + Item[] s = new Item[size]; + for (int i = 0; i < size; ++i) { + s[i] = new Item(i); + } + return s; + } + static Item[] negativeSeqItems(int size) { + Item[] s = new Item[size]; + for (int i = 0; i < size; ++i) { + s[i] = new Item(-i); + } + return s; + } + + // Many tests rely on defaultItems all being sequential nonnegative + public static final Item[] defaultItems = seqItems(SIZE); + + static Item itemFor(int i) { // check cache for defaultItems + Item[] items = defaultItems; + return (i >= 0 && i < items.length) ? items[i] : new Item(i); + } + + public static final Item zero = defaultItems[0]; + public static final Item one = defaultItems[1]; + public static final Item two = defaultItems[2]; + public static final Item three = defaultItems[3]; + public static final Item four = defaultItems[4]; + public static final Item five = defaultItems[5]; + public static final Item six = defaultItems[6]; + public static final Item seven = defaultItems[7]; + public static final Item eight = defaultItems[8]; + public static final Item nine = defaultItems[9]; + public static final Item ten = defaultItems[10]; + + public static final Item[] negativeItems = negativeSeqItems(SIZE); + + public static final Item minusOne = negativeItems[1]; + public static final Item minusTwo = negativeItems[2]; + public static final Item minusThree = negativeItems[3]; + public static final Item minusFour = negativeItems[4]; + public static final Item minusFive = negativeItems[5]; + public static final Item minusSix = negativeItems[6]; + public static final Item minusSeven = negativeItems[7]; + public static final Item minusEight = negativeItems[8]; + public static final Item minusNone = negativeItems[9]; + public static final Item minusTen = negativeItems[10]; + + // elements expected to be missing + public static final Item fortytwo = new Item(42); + public static final Item eightysix = new Item(86); + public static final Item ninetynine = new Item(99); + + // Interop across Item, int + + static void mustEqual(Item x, Item y) { + if (x != y) { + assertEquals(x.value, y.value); + } + } + static void mustEqual(Item x, int y) { + assertEquals(x.value, y); + } + static void mustEqual(int x, Item y) { + assertEquals(x, y.value); + } + static void mustEqual(int x, int y) { + assertEquals(x, y); + } + static void mustEqual(Object x, Object y) { + if (x != y) { + assertEquals(x, y); + } + } + static void mustEqual(int x, Object y) { + if (y instanceof Item) { + assertEquals(x, ((Item)y).value); + } else { + fail(); + } + } + static void mustEqual(Object x, int y) { + if (x instanceof Item) { + assertEquals(((Item)x).value, y); + } else { + fail(); + } + } + static void mustEqual(boolean x, boolean y) { + assertEquals(x, y); + } + static void mustEqual(long x, long y) { + assertEquals(x, y); + } + static void mustEqual(double x, double y) { + assertEquals(x, y); + } + static void mustContain(Collection c, int i) { + assertTrue(c.contains(itemFor(i))); + } + static void mustContain(Collection c, Item i) { + assertTrue(c.contains(i)); + } + static void mustNotContain(Collection c, int i) { + assertFalse(c.contains(itemFor(i))); + } + static void mustNotContain(Collection c, Item i) { + assertFalse(c.contains(i)); + } + static void mustRemove(Collection c, int i) { + assertTrue(c.remove(itemFor(i))); + } + static void mustRemove(Collection c, Item i) { + assertTrue(c.remove(i)); + } + static void mustNotRemove(Collection c, int i) { + Item[] items = defaultItems; + Item x = (i >= 0 && i < items.length) ? items[i] : new Item(i); + assertFalse(c.remove(x)); + } + static void mustNotRemove(Collection c, Item i) { + assertFalse(c.remove(i)); + } + static void mustAdd(Collection c, int i) { + assertTrue(c.add(itemFor(i))); + } + static void mustAdd(Collection c, Item i) { + assertTrue(c.add(i)); + } + static void mustOffer(Queue c, int i) { + assertTrue(c.offer(itemFor(i))); + } + static void mustOffer(Queue c, Item i) { + assertTrue(c.offer(i)); + } + +// /** +// * Runs Runnable r with a security policy that permits precisely +// * the specified permissions. If there is no current security +// * manager, the runnable is run twice, both with and without a +// * security manager. We require that any security manager permit +// * getPolicy/setPolicy. +// */ +// public void runWithPermissions(Runnable r, Permission... permissions) { +// SecurityManager sm = System.getSecurityManager(); +// if (sm == null) { +// r.run(); +// } +// runWithSecurityManagerWithPermissions(r, permissions); +// } +// +// /** +// * Runs Runnable r with a security policy that permits precisely +// * the specified permissions. If there is no current security +// * manager, a temporary one is set for the duration of the +// * Runnable. We require that any security manager permit +// * getPolicy/setPolicy. +// */ +// public void runWithSecurityManagerWithPermissions(Runnable r, +// Permission... permissions) { +// SecurityManager sm = System.getSecurityManager(); +// if (sm == null) { +// Policy savedPolicy = Policy.getPolicy(); +// try { +// Policy.setPolicy(permissivePolicy()); +// System.setSecurityManager(new SecurityManager()); +// runWithSecurityManagerWithPermissions(r, permissions); +// } finally { +// System.setSecurityManager(null); +// Policy.setPolicy(savedPolicy); +// } +// } else { +// Policy savedPolicy = Policy.getPolicy(); +// AdjustablePolicy policy = new AdjustablePolicy(permissions); +// Policy.setPolicy(policy); +// +// try { +// r.run(); +// } finally { +// policy.addPermission(new SecurityPermission("setPolicy")); +// Policy.setPolicy(savedPolicy); +// } +// } +// } +// +// /** +// * Runs a runnable without any permissions. +// */ +// public void runWithoutPermissions(Runnable r) { +// runWithPermissions(r); +// } +// +// /** +// * A security policy where new permissions can be dynamically added +// * or all cleared. +// */ +// public static class AdjustablePolicy extends java.security.Policy { +// Permissions perms = new Permissions(); +// AdjustablePolicy(Permission... permissions) { +// for (Permission permission : permissions) { +// perms.add(permission); +// } +// } +// void addPermission(Permission perm) { perms.add(perm); } +// void clearPermissions() { perms = new Permissions(); } +// @Override +// public PermissionCollection getPermissions(CodeSource cs) { +// return perms; +// } +// @Override +// public PermissionCollection getPermissions(ProtectionDomain pd) { +// return perms; +// } +// @Override +// public boolean implies(ProtectionDomain pd, Permission p) { +// return perms.implies(p); +// } +// @Override +// public void refresh() {} +// @Override +// public String toString() { +// List ps = new ArrayList<>(); +// for (Enumeration e = perms.elements(); e.hasMoreElements();) { +// ps.add(e.nextElement()); +// } +// return "AdjustablePolicy with permissions " + ps; +// } +// } +// +// /** +// * Returns a policy containing all the permissions we ever need. +// */ +// public static Policy permissivePolicy() { +// return new AdjustablePolicy +// // Permissions j.u.c. needs directly +// (new RuntimePermission("modifyThread"), +// new RuntimePermission("getClassLoader"), +// new RuntimePermission("setContextClassLoader"), +// // Permissions needed to change permissions! +// new SecurityPermission("getPolicy"), +// new SecurityPermission("setPolicy"), +// new RuntimePermission("setSecurityManager"), +// // Permissions needed by the junit test harness +// new RuntimePermission("accessDeclaredMembers"), +// new PropertyPermission("*", "read"), +// new java.io.FilePermission("<>", "read")); +// } /** * Sleeps until the given time has elapsed. - * Throws AssertionFailedError if interrupted. + * Throws AssertionError if interrupted. */ - void sleep(long millis) { + static void sleep(long millis) { try { delay(millis); } catch (InterruptedException fail) { - AssertionFailedError afe = - new AssertionFailedError("Unexpected InterruptedException"); - afe.initCause(fail); - throw afe; + throw new AssertionError("Unexpected InterruptedException", fail); } } /** * Spin-waits up to the specified number of milliseconds for the given * thread to enter a wait state: BLOCKED, WAITING, or TIMED_WAITING. + * @param waitingForGodot if non-null, an additional condition to satisfy */ - void waitForThreadToEnterWaitState(Thread thread, long timeoutMillis) { - long startTime = System.nanoTime(); - for (;;) { - Thread.State s = thread.getState(); - if (s == Thread.State.BLOCKED || - s == Thread.State.WAITING || - s == Thread.State.TIMED_WAITING) { - return; - } else if (s == Thread.State.TERMINATED) { - fail("Unexpected thread termination"); + void waitForThreadToEnterWaitState(Thread thread, long timeoutMillis, + Callable waitingForGodot) { + for (long startTime = 0L;;) { + switch (thread.getState()) { + default: break; + case BLOCKED: case WAITING: case TIMED_WAITING: + try { + if (waitingForGodot == null || waitingForGodot.call()) { + return; + } + } catch (Throwable fail) { threadUnexpectedException(fail); } + break; + case TERMINATED: + fail("Unexpected thread termination"); + } + + if (startTime == 0L) { + startTime = System.nanoTime(); } else if (millisElapsedSince(startTime) > timeoutMillis) { - threadAssertTrue(thread.isAlive()); - return; + assertTrue(thread.isAlive()); + if (waitingForGodot == null + || thread.getState() == Thread.State.RUNNABLE) { + fail("timed out waiting for thread to enter wait state"); + } else { + fail("timed out waiting for condition, thread state=" + + thread.getState()); + } } Thread.yield(); } } /** - * Waits up to LONG_DELAY_MS for the given thread to enter a wait - * state: BLOCKED, WAITING, or TIMED_WAITING. + * Spin-waits up to the specified number of milliseconds for the given + * thread to enter a wait state: BLOCKED, WAITING, or TIMED_WAITING. + */ + void waitForThreadToEnterWaitState(Thread thread, long timeoutMillis) { + waitForThreadToEnterWaitState(thread, timeoutMillis, null); + } + + /** + * Spin-waits up to LONG_DELAY_MS milliseconds for the given thread to + * enter a wait state: BLOCKED, WAITING, or TIMED_WAITING. */ void waitForThreadToEnterWaitState(Thread thread) { - waitForThreadToEnterWaitState(thread, LONG_DELAY_MS); + waitForThreadToEnterWaitState(thread, LONG_DELAY_MS, null); + } + + /** + * Spin-waits up to LONG_DELAY_MS milliseconds for the given thread to + * enter a wait state: BLOCKED, WAITING, or TIMED_WAITING, + * and additionally satisfy the given condition. + */ + void waitForThreadToEnterWaitState(Thread thread, + Callable waitingForGodot) { + waitForThreadToEnterWaitState(thread, LONG_DELAY_MS, waitingForGodot); + } + + /** + * Spin-waits up to LONG_DELAY_MS milliseconds for the current thread to + * be interrupted. Clears the interrupt status before returning. + */ + void awaitInterrupted() { + for (long startTime = 0L; !Thread.interrupted(); ) { + if (startTime == 0L) { + startTime = System.nanoTime(); + } else if (millisElapsedSince(startTime) > LONG_DELAY_MS) { + fail("timed out waiting for thread interrupt"); + } + Thread.yield(); + } } /** @@ -712,30 +1633,19 @@ static long millisElapsedSince(long startNanoTime) { return NANOSECONDS.toMillis(System.nanoTime() - startNanoTime); } -// void assertTerminatesPromptly(long timeoutMillis, Runnable r) { -// long startTime = System.nanoTime(); -// try { -// r.run(); -// } catch (Throwable fail) { threadUnexpectedException(fail); } -// if (millisElapsedSince(startTime) > timeoutMillis/2) -// throw new AssertionFailedError("did not return promptly"); -// } - -// void assertTerminatesPromptly(Runnable r) { -// assertTerminatesPromptly(LONG_DELAY_MS/2, r); -// } - /** * Checks that timed f.get() returns the expected value, and does not * wait for the timeout to elapse before returning. */ void checkTimedGet(Future f, T expectedValue, long timeoutMillis) { long startTime = System.nanoTime(); + T actual = null; try { - assertEquals(expectedValue, f.get(timeoutMillis, MILLISECONDS)); + actual = f.get(timeoutMillis, MILLISECONDS); } catch (Throwable fail) { threadUnexpectedException(fail); } + assertEquals(expectedValue, actual); if (millisElapsedSince(startTime) > timeoutMillis/2) { - throw new AssertionFailedError("timed get did not return promptly"); + throw new AssertionError("timed get did not return promptly"); } } @@ -753,21 +1663,34 @@ Thread newStartedThread(Runnable runnable) { return t; } + /** + * Returns a new started daemon Thread running the given action, + * wrapped in a CheckedRunnable. + */ + Thread newStartedThread(Action action) { + return newStartedThread(checkedRunnable(action)); + } + /** * Waits for the specified time (in milliseconds) for the thread * to terminate (using {@link Thread#join(long)}), else interrupts * the thread (in the hope that it may terminate later) and fails. */ - @SuppressWarnings("CatchFail") - void awaitTermination(Thread t, long timeoutMillis) { + void awaitTermination(Thread thread, long timeoutMillis) { try { - t.join(timeoutMillis); + thread.join(timeoutMillis); } catch (InterruptedException fail) { threadUnexpectedException(fail); - } finally { - if (t.getState() != Thread.State.TERMINATED) { - t.interrupt(); - fail("Test timed out"); + } + if (thread.getState() != Thread.State.TERMINATED) { + String detail = String.format( + "timed out waiting for thread to terminate, thread=%s, state=%s" , + thread, thread.getState()); + try { + threadFail(detail); + } finally { + // Interrupt thread __after__ having reported its stack trace + thread.interrupt(); } } } @@ -796,26 +1719,12 @@ public final void run() { } } - public abstract class RunnableShouldThrow implements Runnable { - protected abstract void realRun() throws Throwable; - - final Class exceptionClass; - - RunnableShouldThrow(Class exceptionClass) { - this.exceptionClass = exceptionClass; - } - - @Override - public final void run() { - try { - realRun(); - threadShouldThrow(exceptionClass.getSimpleName()); - } catch (Throwable t) { - if (! exceptionClass.isInstance(t)) { - threadUnexpectedException(t); - } - } - } + Runnable checkedRunnable(Action action) { + return new CheckedRunnable() { + @Override + public void realRun() throws Throwable { + action.run(); + }}; } public abstract class ThreadShouldThrow extends Thread { @@ -831,12 +1740,13 @@ ThreadShouldThrow(Class exceptionClass) { public final void run() { try { realRun(); - threadShouldThrow(exceptionClass.getSimpleName()); } catch (Throwable t) { if (! exceptionClass.isInstance(t)) { threadUnexpectedException(t); } + return; } + threadShouldThrow(exceptionClass.getSimpleName()); } } @@ -847,12 +1757,13 @@ public abstract class CheckedInterruptedRunnable implements Runnable { public final void run() { try { realRun(); - threadShouldThrow("InterruptedException"); } catch (InterruptedException success) { threadAssertFalse(Thread.interrupted()); + return; } catch (Throwable fail) { threadUnexpectedException(fail); } + threadShouldThrow("InterruptedException"); } } @@ -865,27 +1776,8 @@ public final T call() { return realCall(); } catch (Throwable fail) { threadUnexpectedException(fail); - return null; } - } - } - - public abstract class CheckedInterruptedCallable - implements Callable { - protected abstract T realCall() throws Throwable; - - @Override - public final T call() { - try { - T result = realCall(); - threadShouldThrow("InterruptedException"); - return result; - } catch (InterruptedException success) { - threadAssertFalse(Thread.interrupted()); - } catch (Throwable fail) { - threadUnexpectedException(fail); - } - return null; + throw new AssertionError("unreached"); } } @@ -894,7 +1786,7 @@ public static class NoOpRunnable implements Runnable { public void run() {} } - public static class NoOpCallable implements Callable { + public static class NoOpCallable implements Callable { @Override public Object call() { return Boolean.TRUE; } } @@ -902,8 +1794,11 @@ public static class NoOpCallable implements Callable { public static final String TEST_STRING = "a test string"; public static class StringTask implements Callable { + final String value; + public StringTask() { this(TEST_STRING); } + public StringTask(String value) { this.value = value; } @Override - public String call() { return TEST_STRING; } + public String call() { return value; } } public Callable latchAwaitingStringTask(final CountDownLatch latch) { @@ -911,31 +1806,72 @@ public Callable latchAwaitingStringTask(final CountDownLatch latch) { @Override protected String realCall() { try { - assertTrue(latch.await(300, TimeUnit.SECONDS)); + latch.await(); } catch (InterruptedException quittingTime) {} return TEST_STRING; }}; } - public Runnable awaiter(final CountDownLatch latch) { + public Runnable countDowner(final CountDownLatch latch) { return new CheckedRunnable() { @Override public void realRun() throws InterruptedException { - await(latch); + latch.countDown(); }}; } - public void await(CountDownLatch latch) { + class LatchAwaiter extends CheckedRunnable { + static final int NEW = 0; + static final int RUNNING = 1; + static final int DONE = 2; + final CountDownLatch latch; + int state = NEW; + LatchAwaiter(CountDownLatch latch) { this.latch = latch; } + @Override + public void realRun() throws InterruptedException { + state = 1; + await(latch); + state = 2; + } + } + + public LatchAwaiter awaiter(CountDownLatch latch) { + return new LatchAwaiter(latch); + } + + public void await(CountDownLatch latch, long timeoutMillis) { + boolean timedOut = false; try { - assertTrue(latch.await(LONG_DELAY_MS, MILLISECONDS)); + timedOut = !latch.await(timeoutMillis, MILLISECONDS); } catch (Throwable fail) { threadUnexpectedException(fail); } + if (timedOut) { + fail("timed out waiting for CountDownLatch for " + + (timeoutMillis/1000) + " sec"); + } + } + + public void await(CountDownLatch latch) { + await(latch, LONG_DELAY_MS); } public void await(Semaphore semaphore) { + boolean timedOut = false; + try { + timedOut = !semaphore.tryAcquire(LONG_DELAY_MS, MILLISECONDS); + } catch (Throwable fail) { + threadUnexpectedException(fail); + } + if (timedOut) { + fail("timed out waiting for Semaphore for " + + (LONG_DELAY_MS/1000) + " sec"); + } + } + + public void await(CyclicBarrier barrier) { try { - assertTrue(semaphore.tryAcquire(LONG_DELAY_MS, MILLISECONDS)); + barrier.await(LONG_DELAY_MS, MILLISECONDS); } catch (Throwable fail) { threadUnexpectedException(fail); } @@ -955,7 +1891,7 @@ public void await(Semaphore semaphore) { // long startTime = System.nanoTime(); // while (!flag.get()) { // if (millisElapsedSince(startTime) > timeoutMillis) -// throw new AssertionFailedError("timed out"); +// throw new AssertionError("timed out"); // Thread.yield(); // } // } @@ -965,63 +1901,6 @@ public static class NPETask implements Callable { public String call() { throw new NullPointerException(); } } - public static class CallableOne implements Callable { - @Override - public Integer call() { return one; } - } - - public class ShortRunnable extends CheckedRunnable { - @Override - protected void realRun() throws Throwable { - delay(SHORT_DELAY_MS); - } - } - - public class ShortInterruptedRunnable extends CheckedInterruptedRunnable { - @Override - protected void realRun() throws InterruptedException { - delay(SHORT_DELAY_MS); - } - } - - public class SmallRunnable extends CheckedRunnable { - @Override - protected void realRun() throws Throwable { - delay(SMALL_DELAY_MS); - } - } - - public class SmallPossiblyInterruptedRunnable extends CheckedRunnable { - @Override - protected void realRun() { - try { - delay(SMALL_DELAY_MS); - } catch (InterruptedException ok) {} - } - } - - public class SmallCallable extends CheckedCallable { - @Override - protected Object realCall() throws InterruptedException { - delay(SMALL_DELAY_MS); - return Boolean.TRUE; - } - } - - public class MediumRunnable extends CheckedRunnable { - @Override - protected void realRun() throws Throwable { - delay(MEDIUM_DELAY_MS); - } - } - - public class MediumInterruptedRunnable extends CheckedInterruptedRunnable { - @Override - protected void realRun() throws InterruptedException { - delay(MEDIUM_DELAY_MS); - } - } - public Runnable possiblyInterruptedRunnable(final long timeoutMillis) { return new CheckedRunnable() { @Override @@ -1032,24 +1911,6 @@ protected void realRun() { }}; } - public class MediumPossiblyInterruptedRunnable extends CheckedRunnable { - @Override - protected void realRun() { - try { - delay(MEDIUM_DELAY_MS); - } catch (InterruptedException ok) {} - } - } - - public class LongPossiblyInterruptedRunnable extends CheckedRunnable { - @Override - protected void realRun() { - try { - delay(LONG_DELAY_MS); - } catch (InterruptedException ok) {} - } - } - /** * For use as ThreadFactory in constructors */ @@ -1064,65 +1925,6 @@ public interface TrackedRunnable extends Runnable { boolean isDone(); } - public static TrackedRunnable trackedRunnable(final long timeoutMillis) { - return new TrackedRunnable() { - private volatile boolean done = false; - @Override - public boolean isDone() { return done; } - @Override - public void run() { - try { - delay(timeoutMillis); - done = true; - } catch (InterruptedException ok) {} - } - }; - } - - public static class TrackedShortRunnable implements Runnable { - public volatile boolean done = false; - @Override - public void run() { - try { - delay(SHORT_DELAY_MS); - done = true; - } catch (InterruptedException ok) {} - } - } - - public static class TrackedSmallRunnable implements Runnable { - public volatile boolean done = false; - @Override - public void run() { - try { - delay(SMALL_DELAY_MS); - done = true; - } catch (InterruptedException ok) {} - } - } - - public static class TrackedMediumRunnable implements Runnable { - public volatile boolean done = false; - @Override - public void run() { - try { - delay(MEDIUM_DELAY_MS); - done = true; - } catch (InterruptedException ok) {} - } - } - - public static class TrackedLongRunnable implements Runnable { - public volatile boolean done = false; - @Override - public void run() { - try { - delay(LONG_DELAY_MS); - done = true; - } catch (InterruptedException ok) {} - } - } - public static class TrackedNoOpRunnable implements Runnable { public volatile boolean done = false; @Override @@ -1131,18 +1933,6 @@ public void run() { } } - public static class TrackedCallable implements Callable { - public volatile boolean done = false; - @Override - public Object call() { - try { - delay(SMALL_DELAY_MS); - done = true; - } catch (InterruptedException ok) {} - return Boolean.TRUE; - } - } - /** * Analog of CheckedRunnable for RecursiveAction */ @@ -1169,8 +1959,8 @@ public abstract class CheckedRecursiveTask extends RecursiveTask { return realCompute(); } catch (Throwable fail) { threadUnexpectedException(fail); - return null; } + throw new AssertionError("unreached"); } } @@ -1185,33 +1975,30 @@ public void rejectedExecution(Runnable r, /** * A CyclicBarrier that uses timed await and fails with - * AssertionFailedErrors instead of throwing checked exceptions. + * AssertionErrors instead of throwing checked exceptions. */ - public static final class CheckedBarrier extends CyclicBarrier { + public static class CheckedBarrier extends CyclicBarrier { public CheckedBarrier(int parties) { super(parties); } @Override public int await() { try { - return super.await(2 * LONG_DELAY_MS, MILLISECONDS); + return super.await(LONGER_DELAY_MS, MILLISECONDS); } catch (TimeoutException timedOut) { - throw new AssertionFailedError("timed out"); + throw new AssertionError("timed out"); } catch (Exception fail) { - AssertionFailedError afe = - new AssertionFailedError("Unexpected exception: " + fail); - afe.initCause(fail); - throw afe; + throw new AssertionError("Unexpected exception: " + fail, fail); } } } - void checkEmpty(BlockingQueue q) { + void checkEmpty(BlockingQueue q) { try { assertTrue(q.isEmpty()); assertEquals(0, q.size()); assertNull(q.peek()); assertNull(q.poll()); - assertNull(q.poll(0, MILLISECONDS)); + assertNull(q.poll(randomExpiredTimeout(), randomTimeUnit())); assertEquals(q.toString(), "[]"); assertTrue(Arrays.equals(q.toArray(), new Object[0])); assertFalse(q.iterator().hasNext()); @@ -1252,34 +2039,90 @@ byte[] serialBytes(Object o) { } } + @SuppressWarnings("unchecked") + void assertImmutable(Object o) { + if (o instanceof Collection) { + assertThrows( + UnsupportedOperationException.class, + () -> ((Collection) o).add(null)); + } + } + @SuppressWarnings("unchecked") T serialClone(T o) { + T clone = null; try { ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream(serialBytes(o))); - T clone = (T) ois.readObject(); - assertSame(o.getClass(), clone.getClass()); - return clone; + clone = (T) ois.readObject(); } catch (Throwable fail) { threadUnexpectedException(fail); + } + if (o == clone) { + assertImmutable(o); + } else { + assertSame(o.getClass(), clone.getClass()); + } + return clone; + } + + /** + * A version of serialClone that leaves error handling (for + * e.g. NotSerializableException) up to the caller. + */ + @SuppressWarnings("unchecked") + T serialClonePossiblyFailing(T o) + throws ReflectiveOperationException, java.io.IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(o); + oos.flush(); + oos.close(); + ObjectInputStream ois = new ObjectInputStream + (new ByteArrayInputStream(bos.toByteArray())); + T clone = (T) ois.readObject(); + if (o == clone) { + assertImmutable(o); + } else { + assertSame(o.getClass(), clone.getClass()); + } + return clone; + } + + /** + * If o implements Cloneable and has a public clone method, + * returns a clone of o, else null. + */ + @SuppressWarnings("unchecked") + T cloneableClone(T o) { + if (!(o instanceof Cloneable)) { + return null; + } + final T clone; + try { + clone = (T) o.getClass().getMethod("clone").invoke(o); + } catch (NoSuchMethodException ok) { return null; + } catch (ReflectiveOperationException unexpected) { + throw new Error(unexpected); } + assertNotSame(o, clone); // not 100% guaranteed by spec + assertSame(o.getClass(), clone.getClass()); + return clone; } public void assertThrows(Class expectedExceptionClass, - Runnable... throwingActions) { - for (Runnable throwingAction : throwingActions) { + Action... throwingActions) { + for (Action throwingAction : throwingActions) { boolean threw = false; try { throwingAction.run(); } catch (Throwable t) { threw = true; if (!expectedExceptionClass.isInstance(t)) { - AssertionFailedError afe = - new AssertionFailedError - ("Expected " + expectedExceptionClass.getName() + - ", got " + t.getClass().getName()); - afe.initCause(t); - threadUnexpectedException(afe); + throw new AssertionError( + "Expected " + expectedExceptionClass.getName() + + ", got " + t.getClass().getName(), + t); } } if (!threw) { @@ -1295,4 +2138,267 @@ public void assertIteratorExhausted(Iterator it) { } catch (NoSuchElementException success) {} assertFalse(it.hasNext()); } + + public Callable callableThrowing(final Exception ex) { + return new Callable() { @Override + public T call() throws Exception { throw ex; }}; + } + + public Runnable runnableThrowing(final RuntimeException ex) { + return new Runnable() { @Override + public void run() { throw ex; }}; + } + + /** A reusable thread pool to be shared by tests. */ + static final ExecutorService cachedThreadPool = + new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 1000L, MILLISECONDS, + new SynchronousQueue()); + + static void shuffle(T[] array) { + Collections.shuffle(Arrays.asList(array), ThreadLocalRandom.current()); + } + + /** + * Returns the same String as would be returned by {@link + * Object#toString}, whether or not the given object's class + * overrides toString(). + * + * @see System#identityHashCode + */ + static String identityString(Object x) { + return x.getClass().getName() + + "@" + Integer.toHexString(System.identityHashCode(x)); + } + + // --- Shared assertions for Executor tests --- + + /** + * Returns maximum number of tasks that can be submitted to given + * pool (with bounded queue) before saturation (when submission + * throws RejectedExecutionException). + */ + static final int saturatedSize(ThreadPoolExecutor pool) { + BlockingQueue q = pool.getQueue(); + return pool.getMaximumPoolSize() + q.size() + q.remainingCapacity(); + } + + @SuppressWarnings("FutureReturnValueIgnored") + void assertNullTaskSubmissionThrowsNullPointerException(Executor e) { + try { + e.execute((Runnable) null); + shouldThrow(); + } catch (NullPointerException success) {} + + if (! (e instanceof ExecutorService)) { + return; + } + ExecutorService es = (ExecutorService) e; + try { + es.submit((Runnable) null); + shouldThrow(); + } catch (NullPointerException success) {} + try { + es.submit((Runnable) null, Boolean.TRUE); + shouldThrow(); + } catch (NullPointerException success) {} + try { + es.submit((Callable) null); + shouldThrow(); + } catch (NullPointerException success) {} + + if (! (e instanceof ScheduledExecutorService)) { + return; + } + ScheduledExecutorService ses = (ScheduledExecutorService) e; + try { + ses.schedule((Runnable) null, + randomTimeout(), randomTimeUnit()); + shouldThrow(); + } catch (NullPointerException success) {} + try { + ses.schedule((Callable) null, + randomTimeout(), randomTimeUnit()); + shouldThrow(); + } catch (NullPointerException success) {} + try { + ses.scheduleAtFixedRate((Runnable) null, + randomTimeout(), LONG_DELAY_MS, MILLISECONDS); + shouldThrow(); + } catch (NullPointerException success) {} + try { + ses.scheduleWithFixedDelay((Runnable) null, + randomTimeout(), LONG_DELAY_MS, MILLISECONDS); + shouldThrow(); + } catch (NullPointerException success) {} + } + + void setRejectedExecutionHandler( + ThreadPoolExecutor p, RejectedExecutionHandler handler) { + p.setRejectedExecutionHandler(handler); + assertSame(handler, p.getRejectedExecutionHandler()); + } + + void assertTaskSubmissionsAreRejected(ThreadPoolExecutor p) { + final RejectedExecutionHandler savedHandler = p.getRejectedExecutionHandler(); + final long savedTaskCount = p.getTaskCount(); + final long savedCompletedTaskCount = p.getCompletedTaskCount(); + final int savedQueueSize = p.getQueue().size(); + final boolean stock = (p.getClass().getClassLoader() == null); + + Runnable r = () -> {}; + Callable c = () -> Boolean.TRUE; + + class Recorder implements RejectedExecutionHandler { + public volatile Runnable r = null; + public volatile ThreadPoolExecutor p = null; + public void reset() { r = null; p = null; } + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor p) { + assertNull(this.r); + assertNull(this.p); + this.r = r; + this.p = p; + } + } + + // check custom handler is invoked exactly once per task + Recorder recorder = new Recorder(); + setRejectedExecutionHandler(p, recorder); + for (int i = 2; i--> 0; ) { + recorder.reset(); + p.execute(r); + if (stock && p.getClass() == ThreadPoolExecutor.class) { + assertSame(r, recorder.r); + } + assertSame(p, recorder.p); + + recorder.reset(); + assertFalse(p.submit(r).isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + + recorder.reset(); + assertFalse(p.submit(r, Boolean.TRUE).isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + + recorder.reset(); + assertFalse(p.submit(c).isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + + if (p instanceof ScheduledExecutorService) { + ScheduledExecutorService s = (ScheduledExecutorService) p; + ScheduledFuture future; + + recorder.reset(); + future = s.schedule(r, randomTimeout(), randomTimeUnit()); + assertFalse(future.isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + + recorder.reset(); + future = s.schedule(c, randomTimeout(), randomTimeUnit()); + assertFalse(future.isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + + recorder.reset(); + future = s.scheduleAtFixedRate(r, randomTimeout(), LONG_DELAY_MS, MILLISECONDS); + assertFalse(future.isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + + recorder.reset(); + future = s.scheduleWithFixedDelay(r, randomTimeout(), LONG_DELAY_MS, MILLISECONDS); + assertFalse(future.isDone()); + if (stock) { + assertTrue(!((FutureTask) recorder.r).isDone()); + } + assertSame(p, recorder.p); + } + } + + // Checking our custom handler above should be sufficient, but + // we add some integration tests of standard handlers. + final AtomicReference thread = new AtomicReference<>(); + final Runnable setThread = () -> thread.set(Thread.currentThread()); + + setRejectedExecutionHandler(p, new ThreadPoolExecutor.AbortPolicy()); + try { + p.execute(setThread); + shouldThrow(); + } catch (RejectedExecutionException success) {} + assertNull(thread.get()); + + setRejectedExecutionHandler(p, new ThreadPoolExecutor.DiscardPolicy()); + p.execute(setThread); + assertNull(thread.get()); + + setRejectedExecutionHandler(p, new ThreadPoolExecutor.CallerRunsPolicy()); + p.execute(setThread); + if (p.isShutdown()) { + assertNull(thread.get()); + } else { + assertSame(Thread.currentThread(), thread.get()); + } + + setRejectedExecutionHandler(p, savedHandler); + + // check that pool was not perturbed by handlers + assertEquals(savedTaskCount, p.getTaskCount()); + assertEquals(savedCompletedTaskCount, p.getCompletedTaskCount()); + assertEquals(savedQueueSize, p.getQueue().size()); + } + + void assertCollectionsEquals(Collection x, Collection y) { + assertEquals(x, y); + assertEquals(y, x); + assertEquals(x.isEmpty(), y.isEmpty()); + assertEquals(x.size(), y.size()); + if (x instanceof List) { + assertEquals(x.toString(), y.toString()); + } + if (x instanceof List || x instanceof Set) { + assertEquals(x.hashCode(), y.hashCode()); + } + if (x instanceof List || x instanceof Deque) { + assertTrue(Arrays.equals(x.toArray(), y.toArray())); + assertTrue(Arrays.equals(x.toArray(new Object[0]), + y.toArray(new Object[0]))); + } + } + + /** + * A weaker form of assertCollectionsEquals which does not insist + * that the two collections satisfy Object#equals(Object), since + * they may use identity semantics as Deques do. + */ + void assertCollectionsEquivalent(Collection x, Collection y) { + if (x instanceof List || x instanceof Set) { + assertCollectionsEquals(x, y); + } else { + assertEquals(x.isEmpty(), y.isEmpty()); + assertEquals(x.size(), y.size()); + assertEquals(new HashSet(x), new HashSet(y)); + if (x instanceof Deque) { + assertTrue(Arrays.equals(x.toArray(), y.toArray())); + assertTrue(Arrays.equals(x.toArray(new Object[0]), + y.toArray(new Object[0]))); + } + } + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/KeySetTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/KeySetTest.java new file mode 100644 index 0000000000..8c50f62f4e --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/KeySetTest.java @@ -0,0 +1,59 @@ +/* + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import junit.framework.Test; + +@SuppressWarnings("rawtypes") +public class KeySetTest extends JSR166TestCase { + public static void main(String[] args) { + main(suite(), args); + } + + public static Test suite() { + class Implementation implements CollectionImplementation { + final boolean bounded; + + Implementation(boolean bounded) { + this.bounded = bounded; + } + + @Override + public Class klazz() { return Set.class; } + @Override + public Collection emptyCollection() { return set(bounded); } + @Override + public Object makeElement(int i) { return JSR166TestCase.itemFor(i); } + @Override + public boolean isConcurrent() { return true; } + @Override + public boolean permitsNulls() { return false; } + } + return newTestSuite( + KeySetTest.class, + CollectionTest.testSuite(new Implementation(false)), + CollectionTest.testSuite(new Implementation(true))); + } + + private static Set set(boolean bounded) { + var builder = Caffeine.newBuilder(); + if (bounded) { + builder.maximumSize(Integer.MAX_VALUE); + } + Cache cache = builder.build(); + return Collections.newSetFromMap(cache.asMap()); + } + + public void testIgnore() {} +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapImplementation.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapImplementation.java new file mode 100644 index 0000000000..5e5bc42d00 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapImplementation.java @@ -0,0 +1,30 @@ +/* + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import java.util.Map; + +/** Allows tests to work with different Map implementations. */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public interface MapImplementation { + /** Returns the Map implementation class. */ + public Class klazz(); + /** Returns an empty map. */ + public Map emptyMap(); + + // General purpose implementations can use Integers for key and value + default Object makeKey(int i) { return i; } + default Object makeValue(int i) { return i; } + default int keyToInt(Object key) { return (Integer) key; } + default int valueToInt(Object value) { return (Integer) value; } + + public boolean isConcurrent(); + default boolean remappingFunctionCalledAtMostOnce() { return true; }; + public boolean permitsNullKeys(); + public boolean permitsNullValues(); + public boolean supportsSetValue(); +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapTest.java new file mode 100644 index 0000000000..f23e4b4c0c --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/jsr166/MapTest.java @@ -0,0 +1,299 @@ +/* + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.benmanes.caffeine.jsr166; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiFunction; + +import junit.framework.Test; + +/** + * Contains tests applicable to all Map implementations. + */ +@SuppressWarnings({"rawtypes", "unchecked", "HashCodeToString", "UnnecessaryParentheses"}) +public class MapTest extends JSR166TestCase { + final MapImplementation impl; + + /** Tests are parameterized by a Map implementation. */ + MapTest(MapImplementation impl, String methodName) { + super(methodName); + this.impl = impl; + } + + public static Test testSuite(MapImplementation impl) { + return newTestSuite( + parameterizedTestSuite(MapTest.class, + MapImplementation.class, + impl)); + } + + public void testImplSanity() { + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + { + Map m = impl.emptyMap(); + assertTrue(m.isEmpty()); + mustEqual(0, m.size()); + Object k = impl.makeKey(rnd.nextInt()); + Object v = impl.makeValue(rnd.nextInt()); + m.put(k, v); + assertFalse(m.isEmpty()); + mustEqual(1, m.size()); + assertTrue(m.containsKey(k)); + assertTrue(m.containsValue(v)); + } + { + Map m = impl.emptyMap(); + Object v = impl.makeValue(rnd.nextInt()); + if (impl.permitsNullKeys()) { + m.put(null, v); + assertTrue(m.containsKey(null)); + assertTrue(m.containsValue(v)); + } else { + assertThrows(NullPointerException.class, () -> m.put(null, v)); + } + } + { + Map m = impl.emptyMap(); + Object k = impl.makeKey(rnd.nextInt()); + if (impl.permitsNullValues()) { + m.put(k, null); + assertTrue(m.containsKey(k)); + assertTrue(m.containsValue(null)); + } else { + assertThrows(NullPointerException.class, () -> m.put(k, null)); + } + } + { + Map m = impl.emptyMap(); + Object k = impl.makeKey(rnd.nextInt()); + Object v1 = impl.makeValue(rnd.nextInt()); + Object v2 = impl.makeValue(rnd.nextInt()); + m.put(k, v1); + if (impl.supportsSetValue()) { + ((Map.Entry)(m.entrySet().iterator().next())).setValue(v2); + assertSame(v2, m.get(k)); + assertTrue(m.containsKey(k)); + assertTrue(m.containsValue(v2)); + assertFalse(m.containsValue(v1)); + } else { + assertThrows(UnsupportedOperationException.class, + () -> ((Map.Entry)(m.entrySet().iterator().next())).setValue(v2)); + } + } + } + + /** + * Tests and extends the scenario reported in + * https://bugs.openjdk.java.net/browse/JDK-8186171 + * HashMap: Entry.setValue may not work after Iterator.remove() called for previous entries + * ant -Djsr166.tckTestClass=HashMapTest -Djsr166.methodFilter=testBug8186171 -Djsr166.runsPerTest=1000 tck + */ + public void testBug8186171() { + if (!impl.supportsSetValue()) { + return; + } + if (!atLeastJava10()) + { + return; // jdk9 is no longer maintained + } + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + final boolean permitsNullValues = impl.permitsNullValues(); + final Object v1 = (permitsNullValues && rnd.nextBoolean()) + ? null : impl.makeValue(1); + final Object v2 = (permitsNullValues && rnd.nextBoolean() && v1 != null) + ? null : impl.makeValue(2); + + // If true, always lands in first bucket in hash tables. + final boolean poorHash = rnd.nextBoolean(); + class Key implements Comparable { + final int i; + Key(int i) { this.i = i; } + @Override + public int hashCode() { return poorHash ? 0 : super.hashCode(); } + @Override + public int compareTo(Key x) { + return Integer.compare(this.i, x.i); + } + } + + // Both HashMap and ConcurrentHashMap have: + // TREEIFY_THRESHOLD = 8; UNTREEIFY_THRESHOLD = 6; + final int size = rnd.nextInt(1, 25); + + List keys = new ArrayList<>(); + for (int i = size; i-->0; ) { + keys.add(new Key(i)); + } + Key keyToFrob = keys.get(rnd.nextInt(keys.size())); + + Map m = impl.emptyMap(); + for (Key key : keys) { + m.put(key, v1); + } + + for (Iterator> it = m.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getKey() == keyToFrob) { + entry.setValue(v2); // does this have the expected effect? + } else { + it.remove(); + } + } + + assertFalse(m.containsValue(v1)); + assertTrue(m.containsValue(v2)); + assertTrue(m.containsKey(keyToFrob)); + mustEqual(1, m.size()); + } + + /** + * "Missing" test found while investigating JDK-8210280. + * ant -Djsr166.tckTestClass=HashMapTest -Djsr166.methodFilter=testBug8210280 -Djsr166.runsPerTest=1000000 tck + */ + public void testBug8210280() { + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + final int size1 = rnd.nextInt(32); + final int size2 = rnd.nextInt(128); + + final Map m1 = impl.emptyMap(); + for (int i = 0; i < size1; i++) { + int elt = rnd.nextInt(1024 * i, 1024 * (i + 1)); + assertNull(m1.put(impl.makeKey(elt), impl.makeValue(elt))); + } + + final Map m2 = impl.emptyMap(); + for (int i = 0; i < size2; i++) { + int elt = rnd.nextInt(Integer.MIN_VALUE + 1024 * i, + Integer.MIN_VALUE + 1024 * (i + 1)); + assertNull(m2.put(impl.makeKey(elt), impl.makeValue(-elt))); + } + + final Map m1Copy = impl.emptyMap(); + m1Copy.putAll(m1); + + m1.putAll(m2); + + for (Object elt : m2.keySet()) { + mustEqual(m2.get(elt), m1.get(elt)); + } + for (Object elt : m1Copy.keySet()) { + assertSame(m1Copy.get(elt), m1.get(elt)); + } + mustEqual(size1 + size2, m1.size()); + } + + /** + * 8222930: ConcurrentSkipListMap.clone() shares size variable between original and clone + */ + public void testClone() { + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + final int size = rnd.nextInt(4); + final Map map = impl.emptyMap(); + for (int i = 0; i < size; i++) { + map.put(impl.makeKey(i), impl.makeValue(i)); + } + final Map clone = cloneableClone(map); + if (clone == null) + { + return; // not cloneable? + } + + mustEqual(size, map.size()); + mustEqual(size, clone.size()); + mustEqual(map.isEmpty(), clone.isEmpty()); + + clone.put(impl.makeKey(-1), impl.makeValue(-1)); + mustEqual(size, map.size()); + mustEqual(size + 1, clone.size()); + + clone.clear(); + mustEqual(size, map.size()); + mustEqual(0, clone.size()); + assertTrue(clone.isEmpty()); + } + + /** + * Concurrent access by compute methods behaves as expected + */ + public void testConcurrentAccess() throws Throwable { + final Map map = impl.emptyMap(); + final long testDurationMillis = expensiveTests ? 1000 : 2; + final int nTasks = impl.isConcurrent() + ? ThreadLocalRandom.current().nextInt(1, 10) + : 1; + final AtomicBoolean done = new AtomicBoolean(false); + final boolean remappingFunctionCalledAtMostOnce + = impl.remappingFunctionCalledAtMostOnce(); + final List futures = new ArrayList<>(); + final AtomicLong expectedSum = new AtomicLong(0); + final Action[] tasks = { + // repeatedly increment values using compute() + () -> { + long[] invocations = new long[2]; + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + BiFunction incValue = (k, v) -> { + invocations[1]++; + int vi = (v == null) ? 1 : impl.valueToInt(v) + 1; + return impl.makeValue(vi); + }; + while (!done.getAcquire()) { + invocations[0]++; + Object key = impl.makeKey(3 * rnd.nextInt(10)); + map.compute(key, incValue); + } + if (remappingFunctionCalledAtMostOnce) { + mustEqual(invocations[0], invocations[1]); + } + expectedSum.getAndAdd(invocations[0]); + }, + // repeatedly increment values using computeIfPresent() + () -> { + long[] invocations = new long[2]; + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + BiFunction incValue = (k, v) -> { + invocations[1]++; + int vi = impl.valueToInt(v) + 1; + return impl.makeValue(vi); + }; + while (!done.getAcquire()) { + Object key = impl.makeKey(3 * rnd.nextInt(10)); + if (map.computeIfPresent(key, incValue) != null) { + invocations[0]++; + } + } + if (remappingFunctionCalledAtMostOnce) { + mustEqual(invocations[0], invocations[1]); + } + expectedSum.getAndAdd(invocations[0]); + }, + }; + for (int i = nTasks; i--> 0; ) { + Action task = chooseRandomly(tasks); + futures.add(CompletableFuture.runAsync(checkedRunnable(task))); + } + Thread.sleep(testDurationMillis); + done.setRelease(true); + for (var future : futures) { + checkTimedGet(future, null); + } + + long sum = map.values().stream().mapToLong(x -> (int) x).sum(); + mustEqual(expectedSum.get(), sum); + } + +// public void testFailsIntentionallyForDebugging() { +// fail(impl.klazz().getSimpleName()); +// } +}