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