1/*
2 * Written by Doug Lea with assistance from members of JCP JSR-166
3 * Expert Group and released to the public domain, as explained at
4 * http://creativecommons.org/publicdomain/zero/1.0/
5 * Other contributors include Andrew Wright, Jeffrey Hayes,
6 * Pat Fisher, Mike Judd.
7 */
8
9package jsr166;
10
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.Enumeration;
16import java.util.Iterator;
17import java.util.Map;
18import java.util.Random;
19import java.util.Set;
20import java.util.concurrent.ConcurrentHashMap;
21
22import junit.framework.Test;
23import junit.framework.TestSuite;
24
25public class ConcurrentHashMapTest extends JSR166TestCase {
26    // android-note: Removed because the CTS runner does a bad job of
27    // retrying tests that have suite() declarations.
28    //
29    // public static void main(String[] args) {
30    //     main(suite(), args);
31    // }
32    // public static Test suite() {
33    //     return new TestSuite(ConcurrentHashMapTest.class);
34    // }
35
36    /**
37     * Returns a new map from Integers 1-5 to Strings "A"-"E".
38     */
39    private static ConcurrentHashMap<Integer, String> map5() {
40        ConcurrentHashMap map = new ConcurrentHashMap<Integer, String>(5);
41        assertTrue(map.isEmpty());
42        map.put(one, "A");
43        map.put(two, "B");
44        map.put(three, "C");
45        map.put(four, "D");
46        map.put(five, "E");
47        assertFalse(map.isEmpty());
48        assertEquals(5, map.size());
49        return map;
50    }
51
52    /** Re-implement Integer.compare for old java versions */
53    static int compare(int x, int y) {
54        return (x < y) ? -1 : (x > y) ? 1 : 0;
55    }
56
57    // classes for testing Comparable fallbacks
58    static class BI implements Comparable<BI> {
59        private final int value;
60        BI(int value) { this.value = value; }
61        public int compareTo(BI other) {
62            return compare(value, other.value);
63        }
64        public boolean equals(Object x) {
65            return (x instanceof BI) && ((BI)x).value == value;
66        }
67        public int hashCode() { return 42; }
68    }
69    static class CI extends BI { CI(int value) { super(value); } }
70    static class DI extends BI { DI(int value) { super(value); } }
71
72    static class BS implements Comparable<BS> {
73        private final String value;
74        BS(String value) { this.value = value; }
75        public int compareTo(BS other) {
76            return value.compareTo(other.value);
77        }
78        public boolean equals(Object x) {
79            return (x instanceof BS) && value.equals(((BS)x).value);
80        }
81        public int hashCode() { return 42; }
82    }
83
84    static class LexicographicList<E extends Comparable<E>> extends ArrayList<E>
85        implements Comparable<LexicographicList<E>> {
86        LexicographicList(Collection<E> c) { super(c); }
87        LexicographicList(E e) { super(Collections.singleton(e)); }
88        public int compareTo(LexicographicList<E> other) {
89            int common = Math.min(size(), other.size());
90            int r = 0;
91            for (int i = 0; i < common; i++) {
92                if ((r = get(i).compareTo(other.get(i))) != 0)
93                    break;
94            }
95            if (r == 0)
96                r = compare(size(), other.size());
97            return r;
98        }
99        private static final long serialVersionUID = 0;
100    }
101
102    static class CollidingObject {
103        final String value;
104        CollidingObject(final String value) { this.value = value; }
105        public int hashCode() { return this.value.hashCode() & 1; }
106        public boolean equals(final Object obj) {
107            return (obj instanceof CollidingObject) && ((CollidingObject)obj).value.equals(value);
108        }
109    }
110
111    static class ComparableCollidingObject extends CollidingObject implements Comparable<ComparableCollidingObject> {
112        ComparableCollidingObject(final String value) { super(value); }
113        public int compareTo(final ComparableCollidingObject o) {
114            return value.compareTo(o.value);
115        }
116    }
117
118    /**
119     * Inserted elements that are subclasses of the same Comparable
120     * class are found.
121     */
122    public void testComparableFamily() {
123        int size = 500;         // makes measured test run time -> 60ms
124        ConcurrentHashMap<BI, Boolean> m =
125            new ConcurrentHashMap<BI, Boolean>();
126        for (int i = 0; i < size; i++) {
127            assertTrue(m.put(new CI(i), true) == null);
128        }
129        for (int i = 0; i < size; i++) {
130            assertTrue(m.containsKey(new CI(i)));
131            assertTrue(m.containsKey(new DI(i)));
132        }
133    }
134
135    /**
136     * Elements of classes with erased generic type parameters based
137     * on Comparable can be inserted and found.
138     */
139    public void testGenericComparable() {
140        int size = 120;         // makes measured test run time -> 60ms
141        ConcurrentHashMap<Object, Boolean> m =
142            new ConcurrentHashMap<Object, Boolean>();
143        for (int i = 0; i < size; i++) {
144            BI bi = new BI(i);
145            BS bs = new BS(String.valueOf(i));
146            LexicographicList<BI> bis = new LexicographicList<BI>(bi);
147            LexicographicList<BS> bss = new LexicographicList<BS>(bs);
148            assertTrue(m.putIfAbsent(bis, true) == null);
149            assertTrue(m.containsKey(bis));
150            if (m.putIfAbsent(bss, true) == null)
151                assertTrue(m.containsKey(bss));
152            assertTrue(m.containsKey(bis));
153        }
154        for (int i = 0; i < size; i++) {
155            assertTrue(m.containsKey(Collections.singletonList(new BI(i))));
156        }
157    }
158
159    /**
160     * Elements of non-comparable classes equal to those of classes
161     * with erased generic type parameters based on Comparable can be
162     * inserted and found.
163     */
164    public void testGenericComparable2() {
165        int size = 500;         // makes measured test run time -> 60ms
166        ConcurrentHashMap<Object, Boolean> m =
167            new ConcurrentHashMap<Object, Boolean>();
168        for (int i = 0; i < size; i++) {
169            m.put(Collections.singletonList(new BI(i)), true);
170        }
171
172        for (int i = 0; i < size; i++) {
173            LexicographicList<BI> bis = new LexicographicList<BI>(new BI(i));
174            assertTrue(m.containsKey(bis));
175        }
176    }
177
178    /**
179     * Mixtures of instances of comparable and non-comparable classes
180     * can be inserted and found.
181     */
182    public void testMixedComparable() {
183        int size = 1200;        // makes measured test run time -> 35ms
184        ConcurrentHashMap<Object, Object> map =
185            new ConcurrentHashMap<Object, Object>();
186        Random rng = new Random();
187        for (int i = 0; i < size; i++) {
188            Object x;
189            switch (rng.nextInt(4)) {
190            case 0:
191                x = new Object();
192                break;
193            case 1:
194                x = new CollidingObject(Integer.toString(i));
195                break;
196            default:
197                x = new ComparableCollidingObject(Integer.toString(i));
198            }
199            assertNull(map.put(x, x));
200        }
201        int count = 0;
202        for (Object k : map.keySet()) {
203            assertEquals(map.get(k), k);
204            ++count;
205        }
206        assertEquals(count, size);
207        assertEquals(map.size(), size);
208        for (Object k : map.keySet()) {
209            assertEquals(map.put(k, k), k);
210        }
211    }
212
213    /**
214     * clear removes all pairs
215     */
216    public void testClear() {
217        ConcurrentHashMap map = map5();
218        map.clear();
219        assertEquals(0, map.size());
220    }
221
222    /**
223     * Maps with same contents are equal
224     */
225    public void testEquals() {
226        ConcurrentHashMap map1 = map5();
227        ConcurrentHashMap map2 = map5();
228        assertEquals(map1, map2);
229        assertEquals(map2, map1);
230        map1.clear();
231        assertFalse(map1.equals(map2));
232        assertFalse(map2.equals(map1));
233    }
234
235    /**
236     * hashCode() equals sum of each key.hashCode ^ value.hashCode
237     */
238    public void testHashCode() {
239        ConcurrentHashMap<Integer,String> map = map5();
240        int sum = 0;
241        for (Map.Entry<Integer,String> e : map.entrySet())
242            sum += e.getKey().hashCode() ^ e.getValue().hashCode();
243        assertEquals(sum, map.hashCode());
244    }
245
246    /**
247     * contains returns true for contained value
248     */
249    public void testContains() {
250        ConcurrentHashMap map = map5();
251        assertTrue(map.contains("A"));
252        assertFalse(map.contains("Z"));
253    }
254
255    /**
256     * containsKey returns true for contained key
257     */
258    public void testContainsKey() {
259        ConcurrentHashMap map = map5();
260        assertTrue(map.containsKey(one));
261        assertFalse(map.containsKey(zero));
262    }
263
264    /**
265     * containsValue returns true for held values
266     */
267    public void testContainsValue() {
268        ConcurrentHashMap map = map5();
269        assertTrue(map.containsValue("A"));
270        assertFalse(map.containsValue("Z"));
271    }
272
273    /**
274     * enumeration returns an enumeration containing the correct
275     * elements
276     */
277    public void testEnumeration() {
278        ConcurrentHashMap map = map5();
279        Enumeration e = map.elements();
280        int count = 0;
281        while (e.hasMoreElements()) {
282            count++;
283            e.nextElement();
284        }
285        assertEquals(5, count);
286    }
287
288    /**
289     * get returns the correct element at the given key,
290     * or null if not present
291     */
292    public void testGet() {
293        ConcurrentHashMap map = map5();
294        assertEquals("A", (String)map.get(one));
295        ConcurrentHashMap empty = new ConcurrentHashMap();
296        assertNull(map.get("anything"));
297        assertNull(empty.get("anything"));
298    }
299
300    /**
301     * isEmpty is true of empty map and false for non-empty
302     */
303    public void testIsEmpty() {
304        ConcurrentHashMap empty = new ConcurrentHashMap();
305        ConcurrentHashMap map = map5();
306        assertTrue(empty.isEmpty());
307        assertFalse(map.isEmpty());
308    }
309
310    /**
311     * keys returns an enumeration containing all the keys from the map
312     */
313    public void testKeys() {
314        ConcurrentHashMap map = map5();
315        Enumeration e = map.keys();
316        int count = 0;
317        while (e.hasMoreElements()) {
318            count++;
319            e.nextElement();
320        }
321        assertEquals(5, count);
322    }
323
324    /**
325     * keySet returns a Set containing all the keys
326     */
327    public void testKeySet() {
328        ConcurrentHashMap map = map5();
329        Set s = map.keySet();
330        assertEquals(5, s.size());
331        assertTrue(s.contains(one));
332        assertTrue(s.contains(two));
333        assertTrue(s.contains(three));
334        assertTrue(s.contains(four));
335        assertTrue(s.contains(five));
336    }
337
338    /**
339     * keySet.toArray returns contains all keys
340     */
341    public void testKeySetToArray() {
342        ConcurrentHashMap map = map5();
343        Set s = map.keySet();
344        Object[] ar = s.toArray();
345        assertTrue(s.containsAll(Arrays.asList(ar)));
346        assertEquals(5, ar.length);
347        ar[0] = m10;
348        assertFalse(s.containsAll(Arrays.asList(ar)));
349    }
350
351    /**
352     * Values.toArray contains all values
353     */
354    public void testValuesToArray() {
355        ConcurrentHashMap map = map5();
356        Collection v = map.values();
357        Object[] ar = v.toArray();
358        ArrayList s = new ArrayList(Arrays.asList(ar));
359        assertEquals(5, ar.length);
360        assertTrue(s.contains("A"));
361        assertTrue(s.contains("B"));
362        assertTrue(s.contains("C"));
363        assertTrue(s.contains("D"));
364        assertTrue(s.contains("E"));
365    }
366
367    /**
368     * entrySet.toArray contains all entries
369     */
370    public void testEntrySetToArray() {
371        ConcurrentHashMap map = map5();
372        Set s = map.entrySet();
373        Object[] ar = s.toArray();
374        assertEquals(5, ar.length);
375        for (int i = 0; i < 5; ++i) {
376            assertTrue(map.containsKey(((Map.Entry)(ar[i])).getKey()));
377            assertTrue(map.containsValue(((Map.Entry)(ar[i])).getValue()));
378        }
379    }
380
381    /**
382     * values collection contains all values
383     */
384    public void testValues() {
385        ConcurrentHashMap map = map5();
386        Collection s = map.values();
387        assertEquals(5, s.size());
388        assertTrue(s.contains("A"));
389        assertTrue(s.contains("B"));
390        assertTrue(s.contains("C"));
391        assertTrue(s.contains("D"));
392        assertTrue(s.contains("E"));
393    }
394
395    /**
396     * entrySet contains all pairs
397     */
398    public void testEntrySet() {
399        ConcurrentHashMap map = map5();
400        Set s = map.entrySet();
401        assertEquals(5, s.size());
402        Iterator it = s.iterator();
403        while (it.hasNext()) {
404            Map.Entry e = (Map.Entry) it.next();
405            assertTrue(
406                       (e.getKey().equals(one) && e.getValue().equals("A")) ||
407                       (e.getKey().equals(two) && e.getValue().equals("B")) ||
408                       (e.getKey().equals(three) && e.getValue().equals("C")) ||
409                       (e.getKey().equals(four) && e.getValue().equals("D")) ||
410                       (e.getKey().equals(five) && e.getValue().equals("E")));
411        }
412    }
413
414    /**
415     * putAll adds all key-value pairs from the given map
416     */
417    public void testPutAll() {
418        ConcurrentHashMap empty = new ConcurrentHashMap();
419        ConcurrentHashMap map = map5();
420        empty.putAll(map);
421        assertEquals(5, empty.size());
422        assertTrue(empty.containsKey(one));
423        assertTrue(empty.containsKey(two));
424        assertTrue(empty.containsKey(three));
425        assertTrue(empty.containsKey(four));
426        assertTrue(empty.containsKey(five));
427    }
428
429    /**
430     * putIfAbsent works when the given key is not present
431     */
432    public void testPutIfAbsent() {
433        ConcurrentHashMap map = map5();
434        map.putIfAbsent(six, "Z");
435        assertTrue(map.containsKey(six));
436    }
437
438    /**
439     * putIfAbsent does not add the pair if the key is already present
440     */
441    public void testPutIfAbsent2() {
442        ConcurrentHashMap map = map5();
443        assertEquals("A", map.putIfAbsent(one, "Z"));
444    }
445
446    /**
447     * replace fails when the given key is not present
448     */
449    public void testReplace() {
450        ConcurrentHashMap map = map5();
451        assertNull(map.replace(six, "Z"));
452        assertFalse(map.containsKey(six));
453    }
454
455    /**
456     * replace succeeds if the key is already present
457     */
458    public void testReplace2() {
459        ConcurrentHashMap map = map5();
460        assertNotNull(map.replace(one, "Z"));
461        assertEquals("Z", map.get(one));
462    }
463
464    /**
465     * replace value fails when the given key not mapped to expected value
466     */
467    public void testReplaceValue() {
468        ConcurrentHashMap map = map5();
469        assertEquals("A", map.get(one));
470        assertFalse(map.replace(one, "Z", "Z"));
471        assertEquals("A", map.get(one));
472    }
473
474    /**
475     * replace value succeeds when the given key mapped to expected value
476     */
477    public void testReplaceValue2() {
478        ConcurrentHashMap map = map5();
479        assertEquals("A", map.get(one));
480        assertTrue(map.replace(one, "A", "Z"));
481        assertEquals("Z", map.get(one));
482    }
483
484    /**
485     * remove removes the correct key-value pair from the map
486     */
487    public void testRemove() {
488        ConcurrentHashMap map = map5();
489        map.remove(five);
490        assertEquals(4, map.size());
491        assertFalse(map.containsKey(five));
492    }
493
494    /**
495     * remove(key,value) removes only if pair present
496     */
497    public void testRemove2() {
498        ConcurrentHashMap map = map5();
499        map.remove(five, "E");
500        assertEquals(4, map.size());
501        assertFalse(map.containsKey(five));
502        map.remove(four, "A");
503        assertEquals(4, map.size());
504        assertTrue(map.containsKey(four));
505    }
506
507    /**
508     * size returns the correct values
509     */
510    public void testSize() {
511        ConcurrentHashMap map = map5();
512        ConcurrentHashMap empty = new ConcurrentHashMap();
513        assertEquals(0, empty.size());
514        assertEquals(5, map.size());
515    }
516
517    /**
518     * toString contains toString of elements
519     */
520    public void testToString() {
521        ConcurrentHashMap map = map5();
522        String s = map.toString();
523        for (int i = 1; i <= 5; ++i) {
524            assertTrue(s.contains(String.valueOf(i)));
525        }
526    }
527
528    // Exception tests
529
530    /**
531     * Cannot create with only negative capacity
532     */
533    public void testConstructor1() {
534        try {
535            new ConcurrentHashMap(-1);
536            shouldThrow();
537        } catch (IllegalArgumentException success) {}
538    }
539
540    /**
541     * Constructor (initialCapacity, loadFactor) throws
542     * IllegalArgumentException if either argument is negative
543     */
544    public void testConstructor2() {
545        try {
546            new ConcurrentHashMap(-1, .75f);
547            shouldThrow();
548        } catch (IllegalArgumentException success) {}
549
550        try {
551            new ConcurrentHashMap(16, -1);
552            shouldThrow();
553        } catch (IllegalArgumentException success) {}
554    }
555
556    /**
557     * Constructor (initialCapacity, loadFactor, concurrencyLevel)
558     * throws IllegalArgumentException if any argument is negative
559     */
560    public void testConstructor3() {
561        try {
562            new ConcurrentHashMap(-1, .75f, 1);
563            shouldThrow();
564        } catch (IllegalArgumentException success) {}
565
566        try {
567            new ConcurrentHashMap(16, -1, 1);
568            shouldThrow();
569        } catch (IllegalArgumentException success) {}
570
571        try {
572            new ConcurrentHashMap(16, .75f, -1);
573            shouldThrow();
574        } catch (IllegalArgumentException success) {}
575    }
576
577    /**
578     * ConcurrentHashMap(map) throws NullPointerException if the given
579     * map is null
580     */
581    public void testConstructor4() {
582        try {
583            new ConcurrentHashMap(null);
584            shouldThrow();
585        } catch (NullPointerException success) {}
586    }
587
588    /**
589     * ConcurrentHashMap(map) creates a new map with the same mappings
590     * as the given map
591     */
592    public void testConstructor5() {
593        ConcurrentHashMap map1 = map5();
594        ConcurrentHashMap map2 = new ConcurrentHashMap(map5());
595        assertTrue(map2.equals(map1));
596        map2.put(one, "F");
597        assertFalse(map2.equals(map1));
598    }
599
600    /**
601     * get(null) throws NPE
602     */
603    public void testGet_NullPointerException() {
604        ConcurrentHashMap c = new ConcurrentHashMap(5);
605        try {
606            c.get(null);
607            shouldThrow();
608        } catch (NullPointerException success) {}
609    }
610
611    /**
612     * containsKey(null) throws NPE
613     */
614    public void testContainsKey_NullPointerException() {
615        ConcurrentHashMap c = new ConcurrentHashMap(5);
616        try {
617            c.containsKey(null);
618            shouldThrow();
619        } catch (NullPointerException success) {}
620    }
621
622    /**
623     * containsValue(null) throws NPE
624     */
625    public void testContainsValue_NullPointerException() {
626        ConcurrentHashMap c = new ConcurrentHashMap(5);
627        try {
628            c.containsValue(null);
629            shouldThrow();
630        } catch (NullPointerException success) {}
631    }
632
633    /**
634     * contains(null) throws NPE
635     */
636    public void testContains_NullPointerException() {
637        ConcurrentHashMap c = new ConcurrentHashMap(5);
638        try {
639            c.contains(null);
640            shouldThrow();
641        } catch (NullPointerException success) {}
642    }
643
644    /**
645     * put(null,x) throws NPE
646     */
647    public void testPut1_NullPointerException() {
648        ConcurrentHashMap c = new ConcurrentHashMap(5);
649        try {
650            c.put(null, "whatever");
651            shouldThrow();
652        } catch (NullPointerException success) {}
653    }
654
655    /**
656     * put(x, null) throws NPE
657     */
658    public void testPut2_NullPointerException() {
659        ConcurrentHashMap c = new ConcurrentHashMap(5);
660        try {
661            c.put("whatever", null);
662            shouldThrow();
663        } catch (NullPointerException success) {}
664    }
665
666    /**
667     * putIfAbsent(null, x) throws NPE
668     */
669    public void testPutIfAbsent1_NullPointerException() {
670        ConcurrentHashMap c = new ConcurrentHashMap(5);
671        try {
672            c.putIfAbsent(null, "whatever");
673            shouldThrow();
674        } catch (NullPointerException success) {}
675    }
676
677    /**
678     * replace(null, x) throws NPE
679     */
680    public void testReplace_NullPointerException() {
681        ConcurrentHashMap c = new ConcurrentHashMap(5);
682        try {
683            c.replace(null, "whatever");
684            shouldThrow();
685        } catch (NullPointerException success) {}
686    }
687
688    /**
689     * replace(null, x, y) throws NPE
690     */
691    public void testReplaceValue_NullPointerException() {
692        ConcurrentHashMap c = new ConcurrentHashMap(5);
693        try {
694            c.replace(null, one, "whatever");
695            shouldThrow();
696        } catch (NullPointerException success) {}
697    }
698
699    /**
700     * putIfAbsent(x, null) throws NPE
701     */
702    public void testPutIfAbsent2_NullPointerException() {
703        ConcurrentHashMap c = new ConcurrentHashMap(5);
704        try {
705            c.putIfAbsent("whatever", null);
706            shouldThrow();
707        } catch (NullPointerException success) {}
708    }
709
710    /**
711     * replace(x, null) throws NPE
712     */
713    public void testReplace2_NullPointerException() {
714        ConcurrentHashMap c = new ConcurrentHashMap(5);
715        try {
716            c.replace("whatever", null);
717            shouldThrow();
718        } catch (NullPointerException success) {}
719    }
720
721    /**
722     * replace(x, null, y) throws NPE
723     */
724    public void testReplaceValue2_NullPointerException() {
725        ConcurrentHashMap c = new ConcurrentHashMap(5);
726        try {
727            c.replace("whatever", null, "A");
728            shouldThrow();
729        } catch (NullPointerException success) {}
730    }
731
732    /**
733     * replace(x, y, null) throws NPE
734     */
735    public void testReplaceValue3_NullPointerException() {
736        ConcurrentHashMap c = new ConcurrentHashMap(5);
737        try {
738            c.replace("whatever", one, null);
739            shouldThrow();
740        } catch (NullPointerException success) {}
741    }
742
743    /**
744     * remove(null) throws NPE
745     */
746    public void testRemove1_NullPointerException() {
747        ConcurrentHashMap c = new ConcurrentHashMap(5);
748        c.put("sadsdf", "asdads");
749        try {
750            c.remove(null);
751            shouldThrow();
752        } catch (NullPointerException success) {}
753    }
754
755    /**
756     * remove(null, x) throws NPE
757     */
758    public void testRemove2_NullPointerException() {
759        ConcurrentHashMap c = new ConcurrentHashMap(5);
760        c.put("sadsdf", "asdads");
761        try {
762            c.remove(null, "whatever");
763            shouldThrow();
764        } catch (NullPointerException success) {}
765    }
766
767    /**
768     * remove(x, null) returns false
769     */
770    public void testRemove3() {
771        ConcurrentHashMap c = new ConcurrentHashMap(5);
772        c.put("sadsdf", "asdads");
773        assertFalse(c.remove("sadsdf", null));
774    }
775
776    /**
777     * A deserialized map equals original
778     */
779    public void testSerialization() throws Exception {
780        Map x = map5();
781        Map y = serialClone(x);
782
783        assertNotSame(x, y);
784        assertEquals(x.size(), y.size());
785        assertEquals(x, y);
786        assertEquals(y, x);
787    }
788
789    /**
790     * SetValue of an EntrySet entry sets value in the map.
791     */
792    public void testSetValueWriteThrough() {
793        // Adapted from a bug report by Eric Zoerner
794        ConcurrentHashMap map = new ConcurrentHashMap(2, 5.0f, 1);
795        assertTrue(map.isEmpty());
796        for (int i = 0; i < 20; i++)
797            map.put(new Integer(i), new Integer(i));
798        assertFalse(map.isEmpty());
799        Map.Entry entry1 = (Map.Entry)map.entrySet().iterator().next();
800        // Unless it happens to be first (in which case remainder of
801        // test is skipped), remove a possibly-colliding key from map
802        // which, under some implementations, may cause entry1 to be
803        // cloned in map
804        if (!entry1.getKey().equals(new Integer(16))) {
805            map.remove(new Integer(16));
806            entry1.setValue("XYZ");
807            assertTrue(map.containsValue("XYZ")); // fails if write-through broken
808        }
809    }
810
811}
812