1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.atlassian.util.concurrent;
18
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24
25 import org.junit.Test;
26
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 import java.util.concurrent.atomic.AtomicInteger;
36
37 public class CopyOnWriteMapTest {
38
39 @Test public void factoryCalledOnConstructor() {
40 final AtomicInteger count = new AtomicInteger();
41 final Map<String, String> init = MapBuilder.build("1", "o1", "2", "o2", "3", "o3");
42 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
43 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
44 count.getAndIncrement();
45 return new HashMap<String, String>(map);
46 }
47 };
48 assertEquals(1, count.get());
49 assertEquals(3, map.size());
50 assertTrue(map.containsKey("2"));
51 assertTrue(map.containsValue("o3"));
52 assertEquals("o1", map.get("1"));
53 }
54
55 @Test public void factoryCalledOnWrite() {
56 final AtomicInteger count = new AtomicInteger();
57 final Map<String, String> map = new CopyOnWriteMap<String, String>() {
58 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
59 count.getAndIncrement();
60 return new HashMap<String, String>(map);
61 }
62 };
63
64 assertEquals("should be called in ctor", 1, count.get());
65 map.put("test", "test");
66 assertEquals("should be called in put", 2, count.get());
67 assertEquals(1, map.size());
68 assertTrue(map.containsKey("test"));
69 assertTrue(map.containsValue("test"));
70 assertEquals("should not be called in reads", 2, count.get());
71 map.putAll(MapBuilder.build("1", "test1", "2", "test2", "3", "test3"));
72 assertEquals("should be called in putAll", 3, count.get());
73 assertEquals(4, map.size());
74 assertTrue(map.containsKey("1"));
75 assertTrue(map.containsValue("test3"));
76 map.remove("2");
77 assertEquals("should be called in remove", 4, count.get());
78 assertEquals(3, map.size());
79 assertFalse(map.containsValue("test2"));
80 map.clear();
81 assertEquals("should be called in clear", 5, count.get());
82 assertEquals(0, map.size());
83 }
84
85 @Test public void hashAndEquality() throws Exception {
86 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
87 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
88 assertEquals(map, cowMap);
89 assertEquals(map.hashCode(), cowMap.hashCode());
90 }
91
92 @Test public void hashAndEqualityKeySet() throws Exception {
93 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
94 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
95 assertEquals(map.keySet(), cowMap.keySet());
96 assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
97 }
98
99 @Test public void hashAndEqualityValues() throws Exception {
100 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
101 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
102 assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
103 assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
104 }
105
106 @Test public void hashAndEqualityEntrySet() throws Exception {
107 final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
108 final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
109 assertEquals(map.entrySet(), cowMap.entrySet());
110 assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
111 }
112
113 @Test public void modifiableValues() throws Exception {
114 final AtomicInteger count = new AtomicInteger();
115 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
116 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
117 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
118 count.getAndIncrement();
119 return new HashMap<String, String>(map);
120 }
121 };
122 assertEquals(1, count.get());
123 final Collection<String> values = map.values();
124 try {
125 values.add("something");
126 fail("UnsupportedOp expected");
127 }
128 catch (final UnsupportedOperationException ignore) {}
129 assertEquals(1, count.get());
130 try {
131 values.addAll(Arrays.asList("one", "two", "three"));
132 fail("UnsupportedOp expected");
133 }
134 catch (final UnsupportedOperationException ignore) {}
135 final Iterator<String> iterator = values.iterator();
136 assertTrue(iterator.hasNext());
137 assertNotNull(iterator.next());
138 try {
139 iterator.remove();
140 fail("UnsupportedOp expected");
141 }
142 catch (final UnsupportedOperationException ignore) {}
143 assertEquals(1, count.get());
144 assertFalse(values.remove("blah"));
145 assertEquals("not modified if element not present to be removed", 1, count.get());
146 assertTrue(values.remove("test"));
147 assertEquals(2, count.get());
148 assertEquals(1, map.size());
149 assertFalse(values.retainAll(Arrays.asList("testing")));
150 assertEquals(3, count.get());
151 assertEquals(1, map.size());
152 assertTrue(values.removeAll(Arrays.asList("test", "blah", "testing")));
153 assertEquals(4, count.get());
154 assertEquals(0, map.size());
155 }
156
157 @Test public void modifiableEntrySet() throws Exception {
158 final AtomicInteger count = new AtomicInteger();
159 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
160 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
161 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
162 count.getAndIncrement();
163 return new HashMap<String, String>(map);
164 }
165 };
166 assertEquals(1, count.get());
167 final Collection<Entry<String, String>> keys = map.entrySet();
168 class E implements Map.Entry<String, String> {
169 final String e;
170
171 public E(final String e) {
172 this.e = e;
173 }
174
175 public String getKey() {
176 return e;
177 }
178
179 public String getValue() {
180 return e;
181 }
182
183 public String setValue(final String value) {
184 throw new RuntimeException("should not be called, don't use UnsupportedOp here");
185 }
186 }
187
188 try {
189 keys.add(new E("something"));
190 fail("UnsupportedOp expected");
191 }
192 catch (final UnsupportedOperationException ignore) {}
193 assertEquals(1, count.get());
194 try {
195 keys.addAll(Arrays.asList(new E("one"), new E("two"), new E("three")));
196 fail("UnsupportedOp expected");
197 }
198 catch (final UnsupportedOperationException ignore) {}
199 final Iterator<Entry<String, String>> iterator = keys.iterator();
200 assertTrue(iterator.hasNext());
201 assertNotNull(iterator.next());
202 try {
203 iterator.remove();
204 fail("UnsupportedOp expected");
205 }
206 catch (final UnsupportedOperationException ignore) {}
207 assertEquals(1, count.get());
208 assertFalse(keys.remove("blah"));
209 assertEquals("not modified if element not present to be removed", 1, count.get());
210 assertTrue(keys.remove(new E("test")));
211 assertEquals(2, count.get());
212 assertEquals(1, map.size());
213 assertFalse(keys.retainAll(Arrays.asList(new E("testing"))));
214 assertEquals(3, count.get());
215 assertEquals(1, map.size());
216 assertTrue(keys.removeAll(Arrays.asList(new E("test"), new E("blah"), new E("testing"))));
217 assertEquals(4, count.get());
218 assertEquals(0, map.size());
219 }
220
221 @Test public void modifiableKeySet() throws Exception {
222 final AtomicInteger count = new AtomicInteger();
223
224 final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
225 final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
226 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
227 count.getAndIncrement();
228 return new HashMap<String, String>(map);
229 }
230 };
231 assertEquals(1, count.get());
232 final Collection<String> keys = map.keySet();
233 try {
234 keys.add("something");
235 fail("UnsupportedOp expected");
236 }
237 catch (final UnsupportedOperationException ignore) {}
238 assertEquals(1, count.get());
239 try {
240 keys.addAll(Arrays.asList("one", "two", "three"));
241 fail("UnsupportedOp expected");
242 }
243 catch (final UnsupportedOperationException ignore) {}
244 final Iterator<String> iterator = keys.iterator();
245 assertTrue(iterator.hasNext());
246 assertNotNull(iterator.next());
247 try {
248 iterator.remove();
249 fail("UnsupportedOp expected");
250 }
251 catch (final UnsupportedOperationException ignore) {}
252 assertEquals(1, count.get());
253 assertFalse(keys.remove("blah"));
254 assertEquals("not modified if element not present to be removed", 1, count.get());
255 assertTrue(keys.remove("test"));
256 assertEquals(2, count.get());
257 assertEquals(1, map.size());
258 assertFalse(keys.retainAll(Arrays.asList("testing")));
259 assertEquals(3, count.get());
260 assertEquals(1, map.size());
261 assertTrue(keys.removeAll(Arrays.asList("test", "blah", "testing")));
262 assertEquals(4, count.get());
263 assertEquals(0, map.size());
264 }
265
266 @Test public void nullMap() throws Exception {
267 try {
268 new CopyOnWriteMap<String, String>(null) {
269 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
270 return new HashMap<String, String>(map);
271 };
272 };
273 fail("Should have thrown IllegalArgumentEx");
274 }
275 catch (final IllegalArgumentException ignore) {}
276 }
277
278 @Test public void copyFunctionReturnsNull() throws Exception {
279 try {
280 new CopyOnWriteMap<String, String>() {
281 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
282 return null;
283 };
284 };
285 fail("Should have thrown IllegalArgumentEx");
286 }
287 catch (final IllegalArgumentException ignore) {}
288 }
289 }
290
291 class MapBuilder<K, V> {
292 private final Map<K, V> map = new HashMap<K, V>();
293
294 static <S> Map<S, S> build(final S... elements) {
295 if (elements.length % 2 != 0) {
296 throw new IllegalArgumentException("must have even number of elements: " + elements.length);
297 }
298 final MapBuilder<S, S> result = new MapBuilder<S, S>();
299 for (int i = 0; i < elements.length; i = i + 2) {
300 result.add(elements[i], elements[i + 1]);
301 }
302 return result.toMap();
303 }
304
305 static <K, V> MapBuilder<K, V> builder() {
306 return new MapBuilder<K, V>();
307 }
308
309 MapBuilder<K, V> add(final K key, final V value) {
310 map.put(key, value);
311 return this;
312 }
313
314 Entry<K, V> entry(final K key, final V value) {
315 return new Entry<K, V>() {
316 public K getKey() {
317 return key;
318 }
319
320 public V getValue() {
321 return value;
322 }
323
324 public V setValue(final V arg0) {
325 throw new UnsupportedOperationException();
326 };
327 };
328 }
329
330 Map<K, V> toMap() {
331 return Collections.unmodifiableMap(new HashMap<K, V>(map));
332 }
333 }