Skip to content

Commit 70e8cb0

Browse files
committed
Add ConcurrentMap spliterator checks
1 parent 4a71d39 commit 70e8cb0

5 files changed

Lines changed: 264 additions & 1 deletion

File tree

android/guava/src/com/google/common/cache/LocalCache.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
import java.util.NoSuchElementException;
7575
import java.util.Queue;
7676
import java.util.Set;
77+
import java.util.Spliterator;
78+
import java.util.Spliterators;
7779
import java.util.concurrent.Callable;
7880
import java.util.concurrent.ConcurrentLinkedQueue;
7981
import java.util.concurrent.ConcurrentMap;
@@ -4402,6 +4404,12 @@ public boolean isEmpty() {
44024404
public void clear() {
44034405
LocalCache.this.clear();
44044406
}
4407+
4408+
@Override
4409+
public Spliterator<T> spliterator() {
4410+
return Spliterators.spliteratorUnknownSize(
4411+
iterator(), Spliterator.CONCURRENT | Spliterator.DISTINCT | Spliterator.NONNULL);
4412+
}
44054413
}
44064414

44074415
final class KeySet extends AbstractCacheSet<K> {

guava-testlib/src/com/google/common/collect/testing/ConcurrentMapTestSuiteBuilder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.common.collect.testing.testers.ConcurrentMapRemoveTester;
2323
import com.google.common.collect.testing.testers.ConcurrentMapReplaceEntryTester;
2424
import com.google.common.collect.testing.testers.ConcurrentMapReplaceTester;
25+
import com.google.common.collect.testing.testers.ConcurrentMapSpliteratorTester;
2526
import java.util.List;
2627

2728
/**
@@ -44,7 +45,8 @@ public static <K, V> ConcurrentMapTestSuiteBuilder<K, V> using(TestMapGenerator<
4445
ConcurrentMapPutIfAbsentTester.class,
4546
ConcurrentMapRemoveTester.class,
4647
ConcurrentMapReplaceTester.class,
47-
ConcurrentMapReplaceEntryTester.class);
48+
ConcurrentMapReplaceEntryTester.class,
49+
ConcurrentMapSpliteratorTester.class);
4850

4951
@SuppressWarnings("rawtypes") // class literals
5052
@Override
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (C) 2026 The Guava Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.common.collect.testing.testers;
18+
19+
import com.google.common.annotations.GwtCompatible;
20+
import com.google.common.collect.testing.AbstractMapTester;
21+
import java.util.Spliterator;
22+
import java.util.concurrent.ConcurrentMap;
23+
import org.jspecify.annotations.NullMarked;
24+
import org.junit.Ignore;
25+
26+
/**
27+
* A generic JUnit test which tests spliterator characteristics on derived views of a concurrent
28+
* map. Can't be invoked directly; please see {@link
29+
* com.google.common.collect.testing.ConcurrentMapTestSuiteBuilder}.
30+
*
31+
* @author Louis Wasserman
32+
*/
33+
@GwtCompatible
34+
@Ignore("test runners must not instantiate and run this directly, only via suites we build")
35+
// @Ignore affects the Android test runner, which respects JUnit 4 annotations on JUnit 3 tests.
36+
@SuppressWarnings("JUnit4ClassUsedInJUnit3")
37+
@NullMarked
38+
public class ConcurrentMapSpliteratorTester<K, V> extends AbstractMapTester<K, V> {
39+
@Override
40+
protected ConcurrentMap<K, V> getMap() {
41+
return (ConcurrentMap<K, V>) super.getMap();
42+
}
43+
44+
public void testKeySetSpliteratorHasConcurrentCharacteristic() {
45+
assertTrue(
46+
"keySet spliterator should report CONCURRENT",
47+
getMap().keySet().spliterator().hasCharacteristics(Spliterator.CONCURRENT));
48+
}
49+
50+
public void testKeySetSpliteratorDoesNotHaveSizedCharacteristic() {
51+
assertFalse(
52+
"keySet spliterator should not report SIZED",
53+
getMap().keySet().spliterator().hasCharacteristics(Spliterator.SIZED));
54+
}
55+
56+
public void testEntrySetSpliteratorHasConcurrentCharacteristic() {
57+
assertTrue(
58+
"entrySet spliterator should report CONCURRENT",
59+
getMap().entrySet().spliterator().hasCharacteristics(Spliterator.CONCURRENT));
60+
}
61+
62+
public void testEntrySetSpliteratorDoesNotHaveSizedCharacteristic() {
63+
assertFalse(
64+
"entrySet spliterator should not report SIZED",
65+
getMap().entrySet().spliterator().hasCharacteristics(Spliterator.SIZED));
66+
}
67+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (C) 2026 The Guava Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.common.collect.testing;
18+
19+
import com.google.common.collect.ForwardingConcurrentMap;
20+
import com.google.common.collect.testing.features.CollectionFeature;
21+
import com.google.common.collect.testing.features.CollectionSize;
22+
import com.google.common.collect.testing.features.MapFeature;
23+
import java.util.AbstractSet;
24+
import java.util.ArrayList;
25+
import java.util.Iterator;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Map.Entry;
29+
import java.util.Set;
30+
import java.util.Spliterator;
31+
import java.util.Spliterators;
32+
import java.util.concurrent.ConcurrentHashMap;
33+
import java.util.concurrent.ConcurrentMap;
34+
import junit.framework.Test;
35+
import junit.framework.TestCase;
36+
import junit.framework.TestFailure;
37+
import junit.framework.TestResult;
38+
39+
/** Tests for {@link ConcurrentMapTestSuiteBuilder}. */
40+
@AndroidIncompatible // test-suite builders
41+
public final class ConcurrentMapTestSuiteBuilderTest extends TestCase {
42+
public void testConcurrentHashMapDerivedSetSpliteratorsPass() {
43+
TestResult result = runSuite(new ConcurrentHashMapGenerator(), "ConcurrentHashMap");
44+
45+
assertEquals(0, result.errorCount());
46+
assertEquals(0, result.failureCount());
47+
}
48+
49+
public void testConcurrentMapDerivedSetSpliteratorTestsCatchWrongCharacteristics() {
50+
TestResult result = runSuite(new BadSpliteratorConcurrentMapGenerator(), "BadConcurrentMap");
51+
52+
assertEquals(0, result.errorCount());
53+
54+
List<String> failedTests = failedTests(result);
55+
assertTrue(failedTests.toString(), failedTests.size() >= 4);
56+
assertContainsFailure(
57+
failedTests, "testKeySetSpliteratorHasConcurrentCharacteristic");
58+
assertContainsFailure(
59+
failedTests, "testKeySetSpliteratorDoesNotHaveSizedCharacteristic");
60+
assertContainsFailure(
61+
failedTests, "testEntrySetSpliteratorHasConcurrentCharacteristic");
62+
assertContainsFailure(
63+
failedTests, "testEntrySetSpliteratorDoesNotHaveSizedCharacteristic");
64+
}
65+
66+
private static TestResult runSuite(TestStringMapGenerator generator, String name) {
67+
Test suite =
68+
ConcurrentMapTestSuiteBuilder.using(generator)
69+
.named(name)
70+
.withFeatures(
71+
MapFeature.GENERAL_PURPOSE,
72+
CollectionFeature.SUPPORTS_ITERATOR_REMOVE,
73+
CollectionSize.ONE)
74+
.suppressing(new OpenJdk6MapTests().suppressForConcurrentHashMap())
75+
.createTestSuite();
76+
TestResult result = new TestResult();
77+
suite.run(result);
78+
return result;
79+
}
80+
81+
private static List<String> failedTests(TestResult result) {
82+
List<String> failedTests = new ArrayList<>();
83+
for (java.util.Enumeration<TestFailure> failures = result.failures();
84+
failures.hasMoreElements(); ) {
85+
failedTests.add(failures.nextElement().failedTest().toString());
86+
}
87+
return failedTests;
88+
}
89+
90+
private static void assertContainsFailure(List<String> failedTests, String testName) {
91+
for (String failedTest : failedTests) {
92+
if (failedTest.contains(testName)) {
93+
return;
94+
}
95+
}
96+
fail("Expected failure for " + testName + " in " + failedTests);
97+
}
98+
99+
private static final class ConcurrentHashMapGenerator extends TestStringMapGenerator {
100+
@Override
101+
protected Map<String, String> create(Entry<String, String>[] entries) {
102+
ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
103+
for (Entry<String, String> entry : entries) {
104+
map.put(entry.getKey(), entry.getValue());
105+
}
106+
return map;
107+
}
108+
}
109+
110+
private static final class BadSpliteratorConcurrentMapGenerator extends TestStringMapGenerator {
111+
@Override
112+
protected Map<String, String> create(Entry<String, String>[] entries) {
113+
BadSpliteratorConcurrentMap map = new BadSpliteratorConcurrentMap();
114+
for (Entry<String, String> entry : entries) {
115+
map.put(entry.getKey(), entry.getValue());
116+
}
117+
return map;
118+
}
119+
}
120+
121+
private static final class BadSpliteratorConcurrentMap
122+
extends ForwardingConcurrentMap<String, String> {
123+
private final ConcurrentMap<String, String> delegate = new ConcurrentHashMap<>();
124+
125+
@Override
126+
protected ConcurrentMap<String, String> delegate() {
127+
return delegate;
128+
}
129+
130+
@Override
131+
public Set<String> keySet() {
132+
return new BadSpliteratorSet<>(delegate.keySet());
133+
}
134+
135+
@Override
136+
public Set<Entry<String, String>> entrySet() {
137+
return new BadSpliteratorSet<>(delegate.entrySet());
138+
}
139+
}
140+
141+
private static final class BadSpliteratorSet<E> extends AbstractSet<E> {
142+
private final Set<E> delegate;
143+
144+
BadSpliteratorSet(Set<E> delegate) {
145+
this.delegate = delegate;
146+
}
147+
148+
@Override
149+
public Iterator<E> iterator() {
150+
return delegate.iterator();
151+
}
152+
153+
@Override
154+
public int size() {
155+
return delegate.size();
156+
}
157+
158+
@Override
159+
public boolean contains(Object o) {
160+
return delegate.contains(o);
161+
}
162+
163+
@Override
164+
public boolean remove(Object o) {
165+
return delegate.remove(o);
166+
}
167+
168+
@Override
169+
public void clear() {
170+
delegate.clear();
171+
}
172+
173+
@Override
174+
public Spliterator<E> spliterator() {
175+
return Spliterators.spliterator(iterator(), size(), Spliterator.DISTINCT);
176+
}
177+
}
178+
}

guava/src/com/google/common/cache/LocalCache.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
import java.util.NoSuchElementException;
7575
import java.util.Queue;
7676
import java.util.Set;
77+
import java.util.Spliterator;
78+
import java.util.Spliterators;
7779
import java.util.concurrent.Callable;
7880
import java.util.concurrent.ConcurrentLinkedQueue;
7981
import java.util.concurrent.ConcurrentMap;
@@ -4546,6 +4548,12 @@ public boolean isEmpty() {
45464548
public void clear() {
45474549
LocalCache.this.clear();
45484550
}
4551+
4552+
@Override
4553+
public Spliterator<T> spliterator() {
4554+
return Spliterators.spliteratorUnknownSize(
4555+
iterator(), Spliterator.CONCURRENT | Spliterator.DISTINCT | Spliterator.NONNULL);
4556+
}
45494557
}
45504558

45514559
boolean removeIf(BiPredicate<? super K, ? super V> filter) {

0 commit comments

Comments
 (0)