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