1 /*
2  * Copyright 2018 The Android Open Source Project
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 androidx.recyclerview.widget;
18 
19 import static androidx.recyclerview.widget.AdapterHelper.UpdateOp.ADD;
20 import static androidx.recyclerview.widget.AdapterHelper.UpdateOp.MOVE;
21 import static androidx.recyclerview.widget.AdapterHelper.UpdateOp.REMOVE;
22 import static androidx.recyclerview.widget.AdapterHelper.UpdateOp.UPDATE;
23 
24 import static org.junit.Assert.assertEquals;
25 import static org.junit.Assert.assertFalse;
26 
27 import android.util.Log;
28 
29 import androidx.recyclerview.widget.AdapterHelper.UpdateOp;
30 
31 import org.junit.Before;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 import org.junit.runners.JUnit4;
35 
36 import java.util.ArrayList;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Random;
40 import java.util.Set;
41 
42 @RunWith(JUnit4.class)
43 public class OpReorderTest {
44 
45     private static final String TAG = "OpReorderTest";
46 
47     List<UpdateOp> mUpdateOps = new ArrayList<UpdateOp>();
48     List<Item> mAddedItems = new ArrayList<Item>();
49     List<Item> mRemovedItems = new ArrayList<Item>();
50     Set<UpdateOp> mRecycledOps = new HashSet<UpdateOp>();
51     static Random random = new Random(System.nanoTime());
52 
53     OpReorderer mOpReorderer = new OpReorderer(new OpReorderer.Callback() {
54         @Override
55         public UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload) {
56             return new UpdateOp(cmd, startPosition, itemCount, payload);
57         }
58 
59         @Override
60         public void recycleUpdateOp(UpdateOp op) {
61             mRecycledOps.add(op);
62         }
63     });
64 
65     int itemCount = 10;
66     int updatedItemCount = 0;
67 
setup(int count)68     public void setup(int count) {
69         itemCount = count;
70         updatedItemCount = itemCount;
71     }
72 
73     @Before
setUp()74     public void setUp() throws Exception {
75         cleanState();
76     }
77 
cleanState()78     void cleanState() {
79         mUpdateOps = new ArrayList<UpdateOp>();
80         mAddedItems = new ArrayList<Item>();
81         mRemovedItems = new ArrayList<Item>();
82         mRecycledOps = new HashSet<UpdateOp>();
83         Item.idCounter = 0;
84     }
85 
86     @Test
testMoveRemoved()87     public void testMoveRemoved() throws Exception {
88         setup(10);
89         mv(3, 8);
90         rm(7, 3);
91         process();
92     }
93 
94     @Test
testMoveRemove()95     public void testMoveRemove() throws Exception {
96         setup(10);
97         mv(3, 8);
98         rm(3, 5);
99         process();
100     }
101 
102     @Test
test1()103     public void test1() {
104         setup(10);
105         mv(3, 5);
106         rm(3, 4);
107         process();
108     }
109 
110     @Test
test2()111     public void test2() {
112         setup(5);
113         mv(1, 3);
114         rm(1, 1);
115         process();
116     }
117 
118     @Test
test3()119     public void test3() {
120         setup(5);
121         mv(0, 4);
122         rm(2, 1);
123         process();
124     }
125 
126     @Test
test4()127     public void test4() {
128         setup(5);
129         mv(3, 0);
130         rm(3, 1);
131         process();
132     }
133 
134     @Test
test5()135     public void test5() {
136         setup(10);
137         mv(8, 1);
138         rm(6, 3);
139         process();
140     }
141 
142     @Test
test6()143     public void test6() {
144         setup(5);
145         mv(1, 3);
146         rm(0, 3);
147         process();
148     }
149 
150     @Test
test7()151     public void test7() {
152         setup(5);
153         mv(3, 4);
154         rm(3, 1);
155         process();
156     }
157 
158     @Test
test8()159     public void test8() {
160         setup(5);
161         mv(4, 3);
162         rm(3, 1);
163         process();
164     }
165 
166     @Test
test9()167     public void test9() {
168         setup(5);
169         mv(2, 0);
170         rm(2, 2);
171         process();
172     }
173 
174     @Test
testRandom()175     public void testRandom() throws Exception {
176         for (int i = 0; i < 150; i++) {
177             try {
178                 cleanState();
179                 setup(50);
180                 for (int j = 0; j < 50; j++) {
181                     randOp(nextInt(random, nextInt(random, 4)));
182                 }
183                 Log.d(TAG, "running random test " + i);
184                 process();
185             } catch (Throwable t) {
186                 throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps));
187             }
188         }
189     }
190 
191     @Test
testRandomMoveRemove()192     public void testRandomMoveRemove() throws Exception {
193         for (int i = 0; i < 1000; i++) {
194             try {
195                 cleanState();
196                 setup(5);
197                 orderedRandom(MOVE, REMOVE);
198                 process();
199             } catch (Throwable t) {
200                 throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps));
201             }
202         }
203     }
204 
205     @Test
testRandomMoveAdd()206     public void testRandomMoveAdd() throws Exception {
207         for (int i = 0; i < 1000; i++) {
208             try {
209                 cleanState();
210                 setup(5);
211                 orderedRandom(MOVE, ADD);
212                 process();
213             } catch (Throwable t) {
214                 throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps));
215             }
216         }
217     }
218 
219     @Test
testRandomMoveUpdate()220     public void testRandomMoveUpdate() throws Exception {
221         for (int i = 0; i < 1000; i++) {
222             try {
223                 cleanState();
224                 setup(5);
225                 orderedRandom(MOVE, UPDATE);
226                 process();
227             } catch (Throwable t) {
228                 throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps));
229             }
230         }
231     }
232 
opsToString(List<UpdateOp> updateOps)233     private String opsToString(List<UpdateOp> updateOps) {
234         StringBuilder sb = new StringBuilder();
235         for (UpdateOp op : updateOps) {
236             sb.append("\n").append(op.toString());
237         }
238         return sb.append("\n").toString();
239     }
240 
orderedRandom(int... ops)241     public void orderedRandom(int... ops) {
242         for (int op : ops) {
243             randOp(op);
244         }
245     }
246 
randOp(int cmd)247     void randOp(int cmd) {
248         switch (cmd) {
249             case REMOVE:
250                 if (updatedItemCount > 1) {
251                     int s = nextInt(random, updatedItemCount - 1);
252                     int len = Math.max(1, nextInt(random, updatedItemCount - s));
253                     rm(s, len);
254                 }
255                 break;
256             case ADD:
257                 int s = updatedItemCount == 0 ? 0 : nextInt(random, updatedItemCount);
258                 add(s, nextInt(random, 50));
259                 break;
260             case MOVE:
261                 if (updatedItemCount >= 2) {
262                     int from = nextInt(random, updatedItemCount);
263                     int to;
264                     do {
265                         to = nextInt(random, updatedItemCount);
266                     } while (to == from);
267                     mv(from, to);
268                 }
269                 break;
270             case UPDATE:
271                 if (updatedItemCount > 1) {
272                     s = nextInt(random, updatedItemCount - 1);
273                     int len = Math.max(1, nextInt(random, updatedItemCount - s));
274                     up(s, len);
275                 }
276                 break;
277         }
278     }
279 
nextInt(Random random, int n)280     int nextInt(Random random, int n) {
281         if (n == 0) {
282             return 0;
283         }
284         return random.nextInt(n);
285     }
286 
rm(int start, int count)287     UpdateOp rm(int start, int count) {
288         updatedItemCount -= count;
289         return record(new UpdateOp(REMOVE, start, count, null));
290     }
291 
mv(int from, int to)292     UpdateOp mv(int from, int to) {
293         return record(new UpdateOp(MOVE, from, to, null));
294     }
295 
add(int start, int count)296     UpdateOp add(int start, int count) {
297         updatedItemCount += count;
298         return record(new UpdateOp(ADD, start, count, null));
299     }
300 
up(int start, int count)301     UpdateOp up(int start, int count) {
302         return record(new UpdateOp(UPDATE, start, count, null));
303     }
304 
record(UpdateOp op)305     UpdateOp record(UpdateOp op) {
306         mUpdateOps.add(op);
307         return op;
308     }
309 
process()310     void process() {
311         List<Item> items = new ArrayList<Item>(itemCount);
312         for (int i = 0; i < itemCount; i++) {
313             items.add(Item.create());
314         }
315         List<Item> clones = new ArrayList<Item>(itemCount);
316         for (int i = 0; i < itemCount; i++) {
317             clones.add(Item.clone(items.get(i)));
318         }
319         List<UpdateOp> rewritten = rewriteOps(mUpdateOps);
320 
321         assertAllMovesAtTheEnd(rewritten);
322 
323         apply(items, mUpdateOps);
324         List<Item> originalAdded = mAddedItems;
325         List<Item> originalRemoved = mRemovedItems;
326         if (originalAdded.size() > 0) {
327             Item.idCounter = originalAdded.get(0).id;
328         }
329         mAddedItems = new ArrayList<Item>();
330         mRemovedItems = new ArrayList<Item>();
331         apply(clones, rewritten);
332 
333         // now check equality
334         assertListsIdentical(items, clones);
335         assertHasTheSameItems(originalAdded, mAddedItems);
336         assertHasTheSameItems(originalRemoved, mRemovedItems);
337 
338         assertRecycledOpsAreNotReused(items);
339         assertRecycledOpsAreNotReused(clones);
340     }
341 
assertRecycledOpsAreNotReused(List<Item> items)342     private void assertRecycledOpsAreNotReused(List<Item> items) {
343         for (Item item : items) {
344             assertFalse(mRecycledOps.contains(item));
345         }
346     }
347 
assertAllMovesAtTheEnd(List<UpdateOp> ops)348     private void assertAllMovesAtTheEnd(List<UpdateOp> ops) {
349         boolean foundMove = false;
350         for (UpdateOp op : ops) {
351             if (op.cmd == MOVE) {
352                 foundMove = true;
353             } else {
354                 assertFalse(foundMove);
355             }
356         }
357     }
358 
assertHasTheSameItems(List<Item> items, List<Item> clones)359     private void assertHasTheSameItems(List<Item> items,
360             List<Item> clones) {
361         String log = "has the same items\n" + toString(items) + "--\n" + toString(clones);
362         assertEquals(log, items.size(), clones.size());
363         for (Item item : items) {
364             for (Item clone : clones) {
365                 if (item.id == clone.id && item.version == clone.version) {
366                     clones.remove(clone);
367                     break;
368                 }
369             }
370         }
371         assertEquals(log, 0, clones.size());
372     }
373 
assertListsIdentical(List<Item> items, List<Item> clones)374     private void assertListsIdentical(List<Item> items, List<Item> clones) {
375         String log = "is identical\n" + toString(items) + "--\n" + toString(clones);
376         assertEquals(items.size(), clones.size());
377         for (int i = 0; i < items.size(); i++) {
378             Item.assertIdentical(log, items.get(i), clones.get(i));
379         }
380     }
381 
apply(List<Item> items, List<UpdateOp> updateOps)382     private void apply(List<Item> items, List<UpdateOp> updateOps) {
383         for (UpdateOp op : updateOps) {
384             switch (op.cmd) {
385                 case UpdateOp.ADD:
386                     for (int i = 0; i < op.itemCount; i++) {
387                         final Item newItem = Item.create();
388                         mAddedItems.add(newItem);
389                         items.add(op.positionStart + i, newItem);
390                     }
391                     break;
392                 case UpdateOp.REMOVE:
393                     for (int i = 0; i < op.itemCount; i++) {
394                         mRemovedItems.add(items.remove(op.positionStart));
395                     }
396                     break;
397                 case UpdateOp.MOVE:
398                     items.add(op.itemCount, items.remove(op.positionStart));
399                     break;
400                 case UpdateOp.UPDATE:
401                     for (int i = 0; i < op.itemCount; i++) {
402                         final int index = op.positionStart + i;
403                         items.get(index).version = items.get(index).version + 1;
404                     }
405                     break;
406             }
407         }
408     }
409 
rewriteOps(List<UpdateOp> updateOps)410     private List<UpdateOp> rewriteOps(List<UpdateOp> updateOps) {
411         List<UpdateOp> copy = new ArrayList<UpdateOp>();
412         for (UpdateOp op : updateOps) {
413             copy.add(new UpdateOp(op.cmd, op.positionStart, op.itemCount, null));
414         }
415         mOpReorderer.reorderOps(copy);
416         return copy;
417     }
418 
419     @Test
testSwapMoveRemove_1()420     public void testSwapMoveRemove_1() {
421         mv(10, 15);
422         rm(2, 3);
423         swapMoveRemove(mUpdateOps, 0);
424         assertEquals(2, mUpdateOps.size());
425         assertEquals(mv(7, 12), mUpdateOps.get(1));
426         assertEquals(rm(2, 3), mUpdateOps.get(0));
427     }
428 
429     @Test
testSwapMoveRemove_2()430     public void testSwapMoveRemove_2() {
431         mv(3, 8);
432         rm(4, 2);
433         swapMoveRemove(mUpdateOps, 0);
434         assertEquals(2, mUpdateOps.size());
435         assertEquals(rm(5, 2), mUpdateOps.get(0));
436         assertEquals(mv(3, 6), mUpdateOps.get(1));
437     }
438 
439     @Test
testSwapMoveRemove_3()440     public void testSwapMoveRemove_3() {
441         mv(3, 8);
442         rm(3, 2);
443         swapMoveRemove(mUpdateOps, 0);
444         assertEquals(2, mUpdateOps.size());
445         assertEquals(rm(4, 2), mUpdateOps.get(0));
446         assertEquals(mv(3, 6), mUpdateOps.get(1));
447     }
448 
449     @Test
testSwapMoveRemove_4()450     public void testSwapMoveRemove_4() {
451         mv(3, 8);
452         rm(2, 3);
453         swapMoveRemove(mUpdateOps, 0);
454         assertEquals(3, mUpdateOps.size());
455         assertEquals(rm(4, 2), mUpdateOps.get(0));
456         assertEquals(rm(2, 1), mUpdateOps.get(1));
457         assertEquals(mv(2, 5), mUpdateOps.get(2));
458     }
459 
460     @Test
testSwapMoveRemove_5()461     public void testSwapMoveRemove_5() {
462         mv(3, 0);
463         rm(2, 3);
464         swapMoveRemove(mUpdateOps, 0);
465         assertEquals(3, mUpdateOps.size());
466         assertEquals(rm(4, 1), mUpdateOps.get(0));
467         assertEquals(rm(1, 2), mUpdateOps.get(1));
468         assertEquals(mv(1, 0), mUpdateOps.get(2));
469     }
470 
471     @Test
testSwapMoveRemove_6()472     public void testSwapMoveRemove_6() {
473         mv(3, 10);
474         rm(2, 3);
475         swapMoveRemove(mUpdateOps, 0);
476         assertEquals(3, mUpdateOps.size());
477         assertEquals(rm(4, 2), mUpdateOps.get(0));
478         assertEquals(rm(2, 1), mUpdateOps.get(1));
479     }
480 
481     @Test
testSwapMoveRemove_7()482     public void testSwapMoveRemove_7() {
483         mv(3, 2);
484         rm(6, 2);
485         swapMoveRemove(mUpdateOps, 0);
486         assertEquals(2, mUpdateOps.size());
487         assertEquals(rm(6, 2), mUpdateOps.get(0));
488         assertEquals(mv(3, 2), mUpdateOps.get(1));
489     }
490 
491     @Test
testSwapMoveRemove_8()492     public void testSwapMoveRemove_8() {
493         mv(3, 4);
494         rm(3, 1);
495         swapMoveRemove(mUpdateOps, 0);
496         assertEquals(1, mUpdateOps.size());
497         assertEquals(rm(4, 1), mUpdateOps.get(0));
498     }
499 
500     @Test
testSwapMoveRemove_9()501     public void testSwapMoveRemove_9() {
502         mv(3, 4);
503         rm(4, 1);
504         swapMoveRemove(mUpdateOps, 0);
505         assertEquals(1, mUpdateOps.size());
506         assertEquals(rm(3, 1), mUpdateOps.get(0));
507     }
508 
509     @Test
testSwapMoveRemove_10()510     public void testSwapMoveRemove_10() {
511         mv(1, 3);
512         rm(0, 3);
513         swapMoveRemove(mUpdateOps, 0);
514         assertEquals(2, mUpdateOps.size());
515         assertEquals(rm(2, 2), mUpdateOps.get(0));
516         assertEquals(rm(0, 1), mUpdateOps.get(1));
517     }
518 
519     @Test
testSwapMoveRemove_11()520     public void testSwapMoveRemove_11() {
521         mv(3, 8);
522         rm(7, 3);
523         swapMoveRemove(mUpdateOps, 0);
524         assertEquals(2, mUpdateOps.size());
525         assertEquals(rm(3, 1), mUpdateOps.get(0));
526         assertEquals(rm(7, 2), mUpdateOps.get(1));
527     }
528 
529     @Test
testSwapMoveRemove_12()530     public void testSwapMoveRemove_12() {
531         mv(1, 3);
532         rm(2, 1);
533         swapMoveRemove(mUpdateOps, 0);
534         assertEquals(2, mUpdateOps.size());
535         assertEquals(rm(3, 1), mUpdateOps.get(0));
536         assertEquals(mv(1, 2), mUpdateOps.get(1));
537     }
538 
539     @Test
testSwapMoveRemove_13()540     public void testSwapMoveRemove_13() {
541         mv(1, 3);
542         rm(1, 2);
543         swapMoveRemove(mUpdateOps, 0);
544         assertEquals(1, mUpdateOps.size());
545         assertEquals(rm(2, 2), mUpdateOps.get(1));
546     }
547 
548     @Test
testSwapMoveRemove_14()549     public void testSwapMoveRemove_14() {
550         mv(4, 2);
551         rm(3, 1);
552         swapMoveRemove(mUpdateOps, 0);
553         assertEquals(2, mUpdateOps.size());
554         assertEquals(rm(2, 1), mUpdateOps.get(0));
555         assertEquals(mv(2, 3), mUpdateOps.get(1));
556     }
557 
558     @Test
testSwapMoveRemove_15()559     public void testSwapMoveRemove_15() {
560         mv(4, 2);
561         rm(3, 2);
562         swapMoveRemove(mUpdateOps, 0);
563         assertEquals(1, mUpdateOps.size());
564         assertEquals(rm(2, 2), mUpdateOps.get(0));
565     }
566 
567     @Test
testSwapMoveRemove_16()568     public void testSwapMoveRemove_16() {
569         mv(2, 3);
570         rm(1, 2);
571         swapMoveRemove(mUpdateOps, 0);
572         assertEquals(2, mUpdateOps.size());
573         assertEquals(rm(3, 1), mUpdateOps.get(0));
574         assertEquals(rm(1, 1), mUpdateOps.get(1));
575     }
576 
577     @Test
testSwapMoveUpdate_0()578     public void testSwapMoveUpdate_0() {
579         mv(1, 3);
580         up(1, 2);
581         swapMoveUpdate(mUpdateOps, 0);
582         assertEquals(2, mUpdateOps.size());
583         assertEquals(up(2, 2), mUpdateOps.get(0));
584         assertEquals(mv(1, 3), mUpdateOps.get(1));
585     }
586 
587     @Test
testSwapMoveUpdate_1()588     public void testSwapMoveUpdate_1() {
589         mv(0, 2);
590         up(0, 4);
591         swapMoveUpdate(mUpdateOps, 0);
592         assertEquals(3, mUpdateOps.size());
593         assertEquals(up(0, 1), mUpdateOps.get(0));
594         assertEquals(up(1, 3), mUpdateOps.get(1));
595         assertEquals(mv(0, 2), mUpdateOps.get(2));
596     }
597 
598     @Test
testSwapMoveUpdate_2()599     public void testSwapMoveUpdate_2() {
600         mv(2, 0);
601         up(1, 3);
602         swapMoveUpdate(mUpdateOps, 0);
603         assertEquals(3, mUpdateOps.size());
604         assertEquals(up(3, 1), mUpdateOps.get(0));
605         assertEquals(up(0, 2), mUpdateOps.get(1));
606         assertEquals(mv(2, 0), mUpdateOps.get(2));
607     }
608 
swapMoveUpdate(List<UpdateOp> list, int move)609     private void swapMoveUpdate(List<UpdateOp> list, int move) {
610         mOpReorderer.swapMoveUpdate(list, move, list.get(move), move + 1, list.get(move + 1));
611     }
612 
swapMoveRemove(List<UpdateOp> list, int move)613     private void swapMoveRemove(List<UpdateOp> list, int move) {
614         mOpReorderer.swapMoveRemove(list, move, list.get(move), move + 1, list.get(move + 1));
615     }
616 
toString(List<Item> items)617     private String toString(List<Item> items) {
618         StringBuilder sb = new StringBuilder();
619         for (Item item : items) {
620             sb.append(item.toString()).append("\n");
621         }
622         return sb.toString();
623     }
624 
625     static class Item {
626 
627         static int idCounter = 0;
628         int id;
629         int version;
630 
Item(int id, int version)631         Item(int id, int version) {
632             this.id = id;
633             this.version = version;
634         }
635 
create()636         static Item create() {
637             return new Item(idCounter++, 1);
638         }
639 
clone(Item other)640         static Item clone(Item other) {
641             return new Item(other.id, other.version);
642         }
643 
assertIdentical(String logPrefix, Item item1, Item item2)644         public static void assertIdentical(String logPrefix, Item item1, Item item2) {
645             assertEquals(logPrefix + "\n" + item1 + " vs " + item2, item1.id, item2.id);
646             assertEquals(logPrefix + "\n" + item1 + " vs " + item2, item1.version, item2.version);
647         }
648 
649         @Override
toString()650         public String toString() {
651             return "Item{" +
652                     "id=" + id +
653                     ", version=" + version +
654                     '}';
655         }
656     }
657 }
658