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 package androidx.recyclerview.widget; 17 18 import static org.hamcrest.CoreMatchers.equalTo; 19 import static org.hamcrest.CoreMatchers.is; 20 import static org.hamcrest.CoreMatchers.not; 21 import static org.hamcrest.CoreMatchers.nullValue; 22 import static org.hamcrest.MatcherAssert.assertThat; 23 24 import android.support.test.filters.SmallTest; 25 26 import androidx.annotation.Nullable; 27 28 import org.hamcrest.CoreMatchers; 29 import org.junit.Rule; 30 import org.junit.Test; 31 import org.junit.rules.TestWatcher; 32 import org.junit.runner.Description; 33 import org.junit.runner.RunWith; 34 import org.junit.runners.JUnit4; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.Random; 39 import java.util.UUID; 40 41 @RunWith(JUnit4.class) 42 @SmallTest 43 public class DiffUtilTest { 44 private static Random sRand = new Random(System.nanoTime()); 45 private List<Item> mBefore = new ArrayList<>(); 46 private List<Item> mAfter = new ArrayList<>(); 47 private StringBuilder mLog = new StringBuilder(); 48 49 private DiffUtil.Callback mCallback = new DiffUtil.Callback() { 50 @Override 51 public int getOldListSize() { 52 return mBefore.size(); 53 } 54 55 @Override 56 public int getNewListSize() { 57 return mAfter.size(); 58 } 59 60 @Override 61 public boolean areItemsTheSame(int oldItemIndex, int newItemIndex) { 62 return mBefore.get(oldItemIndex).id == mAfter.get(newItemIndex).id; 63 } 64 65 @Override 66 public boolean areContentsTheSame(int oldItemIndex, int newItemIndex) { 67 assertThat(mBefore.get(oldItemIndex).id, 68 CoreMatchers.equalTo(mAfter.get(newItemIndex).id)); 69 return mBefore.get(oldItemIndex).data.equals(mAfter.get(newItemIndex).data); 70 } 71 72 @Nullable 73 @Override 74 public Object getChangePayload(int oldItemIndex, int newItemIndex) { 75 assertThat(mBefore.get(oldItemIndex).id, 76 CoreMatchers.equalTo(mAfter.get(newItemIndex).id)); 77 assertThat(mBefore.get(oldItemIndex).data, 78 not(CoreMatchers.equalTo(mAfter.get(newItemIndex).data))); 79 return mAfter.get(newItemIndex).payload; 80 } 81 }; 82 83 @Rule 84 public TestWatcher mLogOnExceptionWatcher = new TestWatcher() { 85 @Override 86 protected void failed(Throwable e, Description description) { 87 System.err.println(mLog.toString()); 88 } 89 }; 90 91 92 @Test 93 public void testNoChange() { 94 initWithSize(5); 95 check(); 96 } 97 98 @Test 99 public void testAddItems() { 100 initWithSize(2); 101 add(1); 102 check(); 103 } 104 105 //@Test 106 //@LargeTest 107 // Used for development 108 public void testRandom() { 109 for (int x = 0; x < 100; x++) { 110 for (int i = 0; i < 100; i++) { 111 for (int j = 2; j < 40; j++) { 112 testRandom(i, j); 113 } 114 } 115 } 116 } 117 118 @Test 119 public void testGen2() { 120 initWithSize(5); 121 add(5); 122 delete(3); 123 delete(1); 124 check(); 125 } 126 127 @Test 128 public void testGen3() { 129 initWithSize(5); 130 add(0); 131 delete(1); 132 delete(3); 133 check(); 134 } 135 136 @Test 137 public void testGen4() { 138 initWithSize(5); 139 add(5); 140 add(1); 141 add(4); 142 add(4); 143 check(); 144 } 145 146 @Test 147 public void testGen5() { 148 initWithSize(5); 149 delete(0); 150 delete(2); 151 add(0); 152 add(2); 153 check(); 154 } 155 156 @Test 157 public void testGen6() { 158 initWithSize(2); 159 delete(0); 160 delete(0); 161 check(); 162 } 163 164 @Test 165 public void testGen7() { 166 initWithSize(3); 167 move(2, 0); 168 delete(2); 169 add(2); 170 check(); 171 } 172 173 @Test 174 public void testGen8() { 175 initWithSize(3); 176 delete(1); 177 add(0); 178 move(2, 0); 179 check(); 180 } 181 182 @Test 183 public void testGen9() { 184 initWithSize(2); 185 add(2); 186 move(0, 2); 187 check(); 188 } 189 190 @Test 191 public void testGen10() { 192 initWithSize(3); 193 move(0, 1); 194 move(1, 2); 195 add(0); 196 check(); 197 } 198 199 @Test 200 public void testGen11() { 201 initWithSize(4); 202 move(2, 0); 203 move(2, 3); 204 check(); 205 } 206 207 @Test 208 public void testGen12() { 209 initWithSize(4); 210 move(3, 0); 211 move(2, 1); 212 check(); 213 } 214 215 @Test 216 public void testGen13() { 217 initWithSize(4); 218 move(3, 2); 219 move(0, 3); 220 check(); 221 } 222 223 @Test 224 public void testGen14() { 225 initWithSize(4); 226 move(3, 2); 227 add(4); 228 move(0, 4); 229 check(); 230 } 231 232 @Test 233 public void testAdd1() { 234 initWithSize(1); 235 add(1); 236 check(); 237 } 238 239 @Test 240 public void testMove1() { 241 initWithSize(3); 242 move(0, 2); 243 check(); 244 } 245 246 @Test 247 public void tmp() { 248 initWithSize(4); 249 move(0, 2); 250 check(); 251 } 252 253 @Test 254 public void testUpdate1() { 255 initWithSize(3); 256 update(2); 257 check(); 258 } 259 260 @Test 261 public void testUpdate2() { 262 initWithSize(2); 263 add(1); 264 update(1); 265 update(2); 266 check(); 267 } 268 269 @Test 270 public void testDisableMoveDetection() { 271 initWithSize(5); 272 move(0, 4); 273 List<Item> applied = applyUpdates(mBefore, DiffUtil.calculateDiff(mCallback, false)); 274 assertThat(applied.size(), is(5)); 275 assertThat(applied.get(4).newItem, is(true)); 276 assertThat(applied.contains(mBefore.get(0)), is(false)); 277 } 278 279 private void testRandom(int initialSize, int operationCount) { 280 mLog.setLength(0); 281 initWithSize(initialSize); 282 for (int i = 0; i < operationCount; i++) { 283 int op = sRand.nextInt(5); 284 switch (op) { 285 case 0: 286 add(sRand.nextInt(mAfter.size() + 1)); 287 break; 288 case 1: 289 if (!mAfter.isEmpty()) { 290 delete(sRand.nextInt(mAfter.size())); 291 } 292 break; 293 case 2: 294 // move 295 if (mAfter.size() > 0) { 296 move(sRand.nextInt(mAfter.size()), sRand.nextInt(mAfter.size())); 297 } 298 break; 299 case 3: 300 // update 301 if (mAfter.size() > 0) { 302 update(sRand.nextInt(mAfter.size())); 303 } 304 break; 305 case 4: 306 // update with payload 307 if (mAfter.size() > 0) { 308 updateWithPayload(sRand.nextInt(mAfter.size())); 309 } 310 break; 311 } 312 } 313 check(); 314 } 315 316 private void check() { 317 DiffUtil.DiffResult result = DiffUtil.calculateDiff(mCallback); 318 log("before", mBefore); 319 log("after", mAfter); 320 log("snakes", result.getSnakes()); 321 322 List<Item> applied = applyUpdates(mBefore, result); 323 assertEquals(applied, mAfter); 324 } 325 326 private void initWithSize(int size) { 327 mBefore.clear(); 328 mAfter.clear(); 329 for (int i = 0; i < size; i++) { 330 mBefore.add(new Item(false)); 331 } 332 mAfter.addAll(mBefore); 333 mLog.append("initWithSize(" + size + ");\n"); 334 } 335 336 private void log(String title, List<?> items) { 337 mLog.append(title).append(":").append(items.size()).append("\n"); 338 for (Object item : items) { 339 mLog.append(" ").append(item).append("\n"); 340 } 341 } 342 343 private void assertEquals(List<Item> applied, List<Item> after) { 344 log("applied", applied); 345 346 String report = mLog.toString(); 347 assertThat(report, applied.size(), is(after.size())); 348 for (int i = 0; i < after.size(); i++) { 349 Item item = applied.get(i); 350 if (after.get(i).newItem) { 351 assertThat(report, item.newItem, is(true)); 352 } else if (after.get(i).changed) { 353 assertThat(report, item.newItem, is(false)); 354 assertThat(report, item.changed, is(true)); 355 assertThat(report, item.id, is(after.get(i).id)); 356 assertThat(report, item.payload, is(after.get(i).payload)); 357 } else { 358 assertThat(report, item, equalTo(after.get(i))); 359 } 360 } 361 } 362 363 private List<Item> applyUpdates(List<Item> before, DiffUtil.DiffResult result) { 364 final List<Item> target = new ArrayList<>(); 365 target.addAll(before); 366 result.dispatchUpdatesTo(new ListUpdateCallback() { 367 @Override 368 public void onInserted(int position, int count) { 369 for (int i = 0; i < count; i++) { 370 target.add(i + position, new Item(true)); 371 } 372 } 373 374 @Override 375 public void onRemoved(int position, int count) { 376 for (int i = 0; i < count; i++) { 377 target.remove(position); 378 } 379 } 380 381 @Override 382 public void onMoved(int fromPosition, int toPosition) { 383 Item item = target.remove(fromPosition); 384 target.add(toPosition, item); 385 } 386 387 @Override 388 public void onChanged(int position, int count, Object payload) { 389 for (int i = 0; i < count; i++) { 390 int positionInList = position + i; 391 Item existing = target.get(positionInList); 392 // make sure we don't update same item twice in callbacks 393 assertThat(existing.changed, is(false)); 394 assertThat(existing.newItem, is(false)); 395 assertThat(existing.payload, is(nullValue())); 396 Item replica = new Item(existing); 397 replica.payload = (String) payload; 398 replica.changed = true; 399 target.remove(positionInList); 400 target.add(positionInList, replica); 401 } 402 } 403 }); 404 return target; 405 } 406 407 private void add(int index) { 408 mAfter.add(index, new Item(true)); 409 mLog.append("add(").append(index).append(");\n"); 410 } 411 412 private void delete(int index) { 413 mAfter.remove(index); 414 mLog.append("delete(").append(index).append(");\n"); 415 } 416 417 private void update(int index) { 418 Item existing = mAfter.get(index); 419 if (existing.newItem) { 420 return;//new item cannot be changed 421 } 422 Item replica = new Item(existing); 423 replica.changed = true; 424 // clean the payload since this might be after an updateWithPayload call 425 replica.payload = null; 426 replica.data = UUID.randomUUID().toString(); 427 mAfter.remove(index); 428 mAfter.add(index, replica); 429 mLog.append("update(").append(index).append(");\n"); 430 } 431 432 private void updateWithPayload(int index) { 433 Item existing = mAfter.get(index); 434 if (existing.newItem) { 435 return;//new item cannot be changed 436 } 437 Item replica = new Item(existing); 438 replica.changed = true; 439 replica.data = UUID.randomUUID().toString(); 440 replica.payload = UUID.randomUUID().toString(); 441 mAfter.remove(index); 442 mAfter.add(index, replica); 443 mLog.append("update(").append(index).append(");\n"); 444 } 445 446 private void move(int from, int to) { 447 Item removed = mAfter.remove(from); 448 mAfter.add(to, removed); 449 mLog.append("move(").append(from).append(",").append(to).append(");\n"); 450 } 451 452 static class Item { 453 static long idCounter = 0; 454 final long id; 455 final boolean newItem; 456 boolean changed = false; 457 String payload; 458 459 String data = UUID.randomUUID().toString(); 460 461 public Item(boolean newItem) { 462 id = idCounter++; 463 this.newItem = newItem; 464 } 465 466 public Item(Item other) { 467 id = other.id; 468 newItem = other.newItem; 469 changed = other.changed; 470 payload = other.payload; 471 data = other.data; 472 } 473 474 @Override 475 public boolean equals(Object o) { 476 if (this == o) return true; 477 if (o == null || getClass() != o.getClass()) return false; 478 479 Item item = (Item) o; 480 481 if (id != item.id) return false; 482 if (newItem != item.newItem) return false; 483 if (changed != item.changed) return false; 484 if (payload != null ? !payload.equals(item.payload) : item.payload != null) { 485 return false; 486 } 487 return data.equals(item.data); 488 489 } 490 491 @Override 492 public int hashCode() { 493 int result = (int) (id ^ (id >>> 32)); 494 result = 31 * result + (newItem ? 1 : 0); 495 result = 31 * result + (changed ? 1 : 0); 496 result = 31 * result + (payload != null ? payload.hashCode() : 0); 497 result = 31 * result + data.hashCode(); 498 return result; 499 } 500 501 @Override 502 public String toString() { 503 return "Item{" + 504 "id=" + id + 505 ", newItem=" + newItem + 506 ", changed=" + changed + 507 ", payload='" + payload + '\'' + 508 ", data='" + data + '\'' + 509 '}'; 510 } 511 } 512 } 513