1 /* 2 * Copyright (C) 2010 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 com.android.contacts.common.test.mocks; 18 19 import com.google.common.base.Preconditions; 20 import com.google.common.collect.Maps; 21 22 import android.content.ContentValues; 23 import android.database.Cursor; 24 import android.database.MatrixCursor; 25 import android.net.Uri; 26 import android.support.annotation.Nullable; 27 28 import junit.framework.Assert; 29 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Iterator; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Objects; 36 37 /** 38 * A programmable mock content provider. 39 */ 40 public class MockContentProvider extends android.test.mock.MockContentProvider { 41 private static final String TAG = "MockContentProvider"; 42 43 public static class Query { 44 45 private final Uri mUri; 46 private String[] mProjection; 47 private String[] mDefaultProjection; 48 private String mSelection; 49 private String[] mSelectionArgs; 50 private String mSortOrder; 51 private List<Object> mRows = new ArrayList<>(); 52 private boolean mAnyProjection; 53 private boolean mAnySelection; 54 private boolean mAnySortOrder; 55 private boolean mAnyNumberOfTimes; 56 57 private boolean mExecuted; 58 Query(Uri uri)59 public Query(Uri uri) { 60 mUri = uri; 61 } 62 63 @Override toString()64 public String toString() { 65 return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder); 66 } 67 withProjection(String... projection)68 public Query withProjection(String... projection) { 69 mProjection = projection; 70 return this; 71 } 72 withDefaultProjection(String... projection)73 public Query withDefaultProjection(String... projection) { 74 mDefaultProjection = projection; 75 return this; 76 } 77 withAnyProjection()78 public Query withAnyProjection() { 79 mAnyProjection = true; 80 return this; 81 } 82 withSelection(String selection, String... selectionArgs)83 public Query withSelection(String selection, String... selectionArgs) { 84 mSelection = selection; 85 mSelectionArgs = selectionArgs; 86 return this; 87 } 88 withAnySelection()89 public Query withAnySelection() { 90 mAnySelection = true; 91 return this; 92 } 93 withSortOrder(String sortOrder)94 public Query withSortOrder(String sortOrder) { 95 mSortOrder = sortOrder; 96 return this; 97 } 98 withAnySortOrder()99 public Query withAnySortOrder() { 100 mAnySortOrder = true; 101 return this; 102 } 103 returnRow(ContentValues values)104 public Query returnRow(ContentValues values) { 105 mRows.add(values); 106 return this; 107 } 108 returnRow(Object... row)109 public Query returnRow(Object... row) { 110 mRows.add(row); 111 return this; 112 } 113 returnEmptyCursor()114 public Query returnEmptyCursor() { 115 mRows.clear(); 116 return this; 117 } 118 anyNumberOfTimes()119 public Query anyNumberOfTimes() { 120 mAnyNumberOfTimes = true; 121 return this; 122 } 123 equals(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)124 public boolean equals(Uri uri, String[] projection, String selection, 125 String[] selectionArgs, String sortOrder) { 126 if (!uri.equals(mUri)) { 127 return false; 128 } 129 130 if (!mAnyProjection && !Arrays.equals(projection, mProjection)) { 131 return false; 132 } 133 134 if (!mAnySelection && !Objects.equals(selection, mSelection)) { 135 return false; 136 } 137 138 if (!mAnySelection && !Arrays.equals(selectionArgs, mSelectionArgs)) { 139 return false; 140 } 141 142 if (!mAnySortOrder && !Objects.equals(sortOrder, mSortOrder)) { 143 return false; 144 } 145 146 return true; 147 } 148 getResult(String[] projection)149 public Cursor getResult(String[] projection) { 150 String[] columnNames; 151 if (mAnyProjection) { 152 columnNames = projection; 153 } else { 154 columnNames = mProjection != null ? mProjection : mDefaultProjection; 155 } 156 157 MatrixCursor cursor = new MatrixCursor(columnNames); 158 for (Object row : mRows) { 159 if (row instanceof Object[]) { 160 cursor.addRow((Object[]) row); 161 } else { 162 ContentValues values = (ContentValues) row; 163 Object[] columns = new Object[projection.length]; 164 for (int i = 0; i < projection.length; i++) { 165 columns[i] = values.get(projection[i]); 166 } 167 cursor.addRow(columns); 168 } 169 } 170 return cursor; 171 } 172 } 173 174 public static class TypeQuery { 175 private final Uri mUri; 176 private final String mType; 177 TypeQuery(Uri uri, String type)178 public TypeQuery(Uri uri, String type) { 179 mUri = uri; 180 mType = type; 181 } 182 getUri()183 public Uri getUri() { 184 return mUri; 185 } 186 getType()187 public String getType() { 188 return mType; 189 } 190 191 @Override toString()192 public String toString() { 193 return mUri + " --> " + mType; 194 } 195 equals(Uri uri)196 public boolean equals(Uri uri) { 197 return getUri().equals(uri); 198 } 199 } 200 201 public static class Insert { 202 private final Uri mUri; 203 private final ContentValues mContentValues; 204 private final Uri mResultUri; 205 private boolean mAnyNumberOfTimes; 206 private boolean mIsExecuted; 207 208 /** 209 * Creates a new Insert to expect. 210 * 211 * @param uri the uri of the insertion request. 212 * @param contentValues the ContentValues to insert. 213 * @param resultUri the {@link Uri} for the newly inserted item. 214 * @throws NullPointerException if any parameter is {@code null}. 215 */ Insert(Uri uri, ContentValues contentValues, Uri resultUri)216 public Insert(Uri uri, ContentValues contentValues, Uri resultUri) { 217 mUri = Preconditions.checkNotNull(uri); 218 mContentValues = Preconditions.checkNotNull(contentValues); 219 mResultUri = Preconditions.checkNotNull(resultUri); 220 } 221 222 /** 223 * Causes this insert expectation to be useable for mutliple calls to insert, rather than 224 * just one. 225 * 226 * @return this 227 */ anyNumberOfTimes()228 public Insert anyNumberOfTimes() { 229 mAnyNumberOfTimes = true; 230 return this; 231 } 232 equals(Uri uri, ContentValues contentValues)233 private boolean equals(Uri uri, ContentValues contentValues) { 234 return mUri.equals(uri) && mContentValues.equals(contentValues); 235 } 236 237 @Override equals(Object o)238 public boolean equals(Object o) { 239 if (this == o) { 240 return true; 241 } 242 if (o == null || getClass() != o.getClass()) { 243 return false; 244 } 245 Insert insert = (Insert) o; 246 return mAnyNumberOfTimes == insert.mAnyNumberOfTimes && 247 mIsExecuted == insert.mIsExecuted && 248 Objects.equals(mUri, insert.mUri) && 249 Objects.equals(mContentValues, insert.mContentValues) && 250 Objects.equals(mResultUri, insert.mResultUri); 251 } 252 253 @Override hashCode()254 public int hashCode() { 255 return Objects.hash(mUri, mContentValues, mResultUri, mAnyNumberOfTimes, mIsExecuted); 256 } 257 258 @Override toString()259 public String toString() { 260 return "Insert{" + 261 "mUri=" + mUri + 262 ", mContentValues=" + mContentValues + 263 ", mResultUri=" + mResultUri + 264 ", mAnyNumberOfTimes=" + mAnyNumberOfTimes + 265 ", mIsExecuted=" + mIsExecuted + 266 '}'; 267 } 268 } 269 270 public static class Delete { 271 private final Uri mUri; 272 273 private boolean mAnyNumberOfTimes; 274 private boolean mAnySelection; 275 @Nullable private String mSelection; 276 @Nullable private String[] mSelectionArgs; 277 private boolean mIsExecuted; 278 private int mRowsAffected; 279 280 /** 281 * Creates a new Delete to expect. 282 * @param uri the uri of the delete request. 283 * @throws NullPointerException if uri is {@code null}. 284 */ Delete(Uri uri)285 public Delete(Uri uri) { 286 mUri = Preconditions.checkNotNull(uri); 287 } 288 289 /** 290 * Sets the given information as expected selection arguments. 291 * 292 * @param selection The selection to expect. 293 * @param selectionArgs The selection args to expect. 294 * @return this. 295 */ withSelection(String selection, @Nullable String[] selectionArgs)296 public Delete withSelection(String selection, @Nullable String[] selectionArgs) { 297 mSelection = Preconditions.checkNotNull(selection); 298 mSelectionArgs = selectionArgs; 299 mAnySelection = false; 300 return this; 301 } 302 303 /** 304 * Sets this delete to expect any selection arguments. 305 * 306 * @return this. 307 */ withAnySelection()308 public Delete withAnySelection() { 309 mAnySelection = true; 310 return this; 311 } 312 313 /** 314 * Sets this delete to return the given number of rows affected. 315 * 316 * @param rowsAffected The value to return when this expected delete is executed. 317 * @return this. 318 */ returnRowsAffected(int rowsAffected)319 public Delete returnRowsAffected(int rowsAffected) { 320 mRowsAffected = rowsAffected; 321 return this; 322 } 323 324 /** 325 * Causes this delete expectation to be useable for multiple calls to delete, rather than 326 * just one. 327 * 328 * @return this. 329 */ anyNumberOfTimes()330 public Delete anyNumberOfTimes() { 331 mAnyNumberOfTimes = true; 332 return this; 333 } 334 equals(Uri uri, String selection, String[] selectionArgs)335 private boolean equals(Uri uri, String selection, String[] selectionArgs) { 336 return mUri.equals(uri) && Objects.equals(mSelection, selection) 337 && Arrays.equals(mSelectionArgs, selectionArgs); 338 } 339 } 340 341 public static class Update { 342 private final Uri mUri; 343 private final ContentValues mContentValues; 344 @Nullable private String mSelection; 345 @Nullable private String[] mSelectionArgs; 346 private boolean mAnyNumberOfTimes; 347 private boolean mIsExecuted; 348 private int mRowsAffected; 349 350 /** 351 * Creates a new Update to expect. 352 * 353 * @param uri the uri of the update request. 354 * @param contentValues the ContentValues to update. 355 * 356 * @throws NullPointerException if any parameter is {@code null}. 357 */ Update(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)358 public Update(Uri uri, 359 ContentValues contentValues, 360 @Nullable String selection, 361 @Nullable String[] selectionArgs) { 362 mUri = Preconditions.checkNotNull(uri); 363 mContentValues = Preconditions.checkNotNull(contentValues); 364 mSelection = selection; 365 mSelectionArgs = selectionArgs; 366 } 367 368 /** 369 * Causes this update expectation to be useable for mutliple calls to update, rather than 370 * just one. 371 * 372 * @return this 373 */ anyNumberOfTimes()374 public Update anyNumberOfTimes() { 375 mAnyNumberOfTimes = true; 376 return this; 377 } 378 379 /** 380 * Sets this update to return the given number of rows affected. 381 * 382 * @param rowsAffected The value to return when this expected update is executed. 383 * @return this. 384 */ returnRowsAffected(int rowsAffected)385 public Update returnRowsAffected(int rowsAffected) { 386 mRowsAffected = rowsAffected; 387 return this; 388 } 389 equals(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)390 private boolean equals(Uri uri, 391 ContentValues contentValues, 392 @Nullable String selection, 393 @Nullable String[] selectionArgs) { 394 return mUri.equals(uri) && mContentValues.equals(contentValues) && 395 Objects.equals(mSelection, selection) && 396 Objects.equals(mSelectionArgs, selectionArgs); 397 } 398 399 @Override equals(Object o)400 public boolean equals(Object o) { 401 if (this == o) { 402 return true; 403 } 404 if (o == null || getClass() != o.getClass()) { 405 return false; 406 } 407 Update update = (Update) o; 408 return mAnyNumberOfTimes == update.mAnyNumberOfTimes && 409 mIsExecuted == update.mIsExecuted && 410 Objects.equals(mUri, update.mUri) && 411 Objects.equals(mContentValues, update.mContentValues) && 412 Objects.equals(mSelection, update.mSelection) && 413 Objects.equals(mSelectionArgs, update.mSelectionArgs); 414 } 415 416 @Override hashCode()417 public int hashCode() { 418 return Objects.hash(mUri, mContentValues, mAnyNumberOfTimes, mIsExecuted, mSelection, 419 mSelectionArgs); 420 } 421 422 @Override toString()423 public String toString() { 424 return "Update{" + 425 "mUri=" + mUri + 426 ", mContentValues=" + mContentValues + 427 ", mAnyNumberOfTimes=" + mAnyNumberOfTimes + 428 ", mIsExecuted=" + mIsExecuted + 429 ", mSelection=" + mSelection + 430 ", mSelectionArgs=" + mSelectionArgs + 431 '}'; 432 } 433 } 434 435 private List<Query> mExpectedQueries = new ArrayList<>(); 436 private Map<Uri, String> mExpectedTypeQueries = Maps.newHashMap(); 437 private List<Insert> mExpectedInserts = new ArrayList<>(); 438 private List<Delete> mExpectedDeletes = new ArrayList<>(); 439 private List<Update> mExpectedUpdates = new ArrayList<>(); 440 441 @Override onCreate()442 public boolean onCreate() { 443 return true; 444 } 445 expectQuery(Uri contentUri)446 public Query expectQuery(Uri contentUri) { 447 Query query = new Query(contentUri); 448 mExpectedQueries.add(query); 449 return query; 450 } 451 expectTypeQuery(Uri uri, String type)452 public void expectTypeQuery(Uri uri, String type) { 453 mExpectedTypeQueries.put(uri, type); 454 } 455 expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri)456 public void expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri) { 457 mExpectedInserts.add(new Insert(contentUri, contentValues, resultUri)); 458 } 459 expectUpdate(Uri contentUri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)460 public Update expectUpdate(Uri contentUri, 461 ContentValues contentValues, 462 @Nullable String selection, 463 @Nullable String[] selectionArgs) { 464 Update update = new Update(contentUri, contentValues, selection, selectionArgs); 465 mExpectedUpdates.add(update); 466 return update; 467 } 468 expectDelete(Uri contentUri)469 public Delete expectDelete(Uri contentUri) { 470 Delete delete = new Delete(contentUri); 471 mExpectedDeletes.add(delete); 472 return delete; 473 } 474 475 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)476 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 477 String sortOrder) { 478 if (mExpectedQueries.isEmpty()) { 479 Assert.fail("Unexpected query: Actual:" 480 + queryToString(uri, projection, selection, selectionArgs, sortOrder)); 481 } 482 483 for (Iterator<Query> iterator = mExpectedQueries.iterator(); iterator.hasNext();) { 484 Query query = iterator.next(); 485 if (query.equals(uri, projection, selection, selectionArgs, sortOrder)) { 486 query.mExecuted = true; 487 if (!query.mAnyNumberOfTimes) { 488 iterator.remove(); 489 } 490 return query.getResult(projection); 491 } 492 } 493 494 Assert.fail("Incorrect query. Expected one of: " + mExpectedQueries + ". Actual: " + 495 queryToString(uri, projection, selection, selectionArgs, sortOrder)); 496 return null; 497 } 498 499 @Override getType(Uri uri)500 public String getType(Uri uri) { 501 if (mExpectedTypeQueries.isEmpty()) { 502 Assert.fail("Unexpected getType query: " + uri); 503 } 504 505 String mimeType = mExpectedTypeQueries.get(uri); 506 if (mimeType != null) { 507 return mimeType; 508 } 509 510 Assert.fail("Unknown mime type for: " + uri); 511 return null; 512 } 513 514 @Override insert(Uri uri, ContentValues values)515 public Uri insert(Uri uri, ContentValues values) { 516 if (mExpectedInserts.isEmpty()) { 517 Assert.fail("Unexpected insert. Actual: " + insertToString(uri, values)); 518 } 519 for (Iterator<Insert> iterator = mExpectedInserts.iterator(); iterator.hasNext(); ) { 520 Insert insert = iterator.next(); 521 if (insert.equals(uri, values)) { 522 insert.mIsExecuted = true; 523 if (!insert.mAnyNumberOfTimes) { 524 iterator.remove(); 525 } 526 return insert.mResultUri; 527 } 528 } 529 530 Assert.fail("Incorrect insert. Expected one of: " + mExpectedInserts + ". Actual: " 531 + insertToString(uri, values)); 532 return null; 533 } 534 insertToString(Uri uri, ContentValues contentValues)535 private String insertToString(Uri uri, ContentValues contentValues) { 536 return "Insert { uri=" + uri + ", contentValues=" + contentValues + '}'; 537 } 538 539 @Override update(Uri uri, ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)540 public int update(Uri uri, 541 ContentValues values, 542 @Nullable String selection, 543 @Nullable String[] selectionArgs) { 544 if (mExpectedUpdates.isEmpty()) { 545 Assert.fail("Unexpected update. Actual: " 546 + updateToString(uri, values, selection, selectionArgs)); 547 } 548 for (Iterator<Update> iterator = mExpectedUpdates.iterator(); iterator.hasNext(); ) { 549 Update update = iterator.next(); 550 if (update.equals(uri, values, selection, selectionArgs)) { 551 update.mIsExecuted = true; 552 if (!update.mAnyNumberOfTimes) { 553 iterator.remove(); 554 } 555 return update.mRowsAffected; 556 } 557 } 558 559 Assert.fail("Incorrect update. Expected one of: " + mExpectedUpdates + ". Actual: " 560 + updateToString(uri, values, selection, selectionArgs)); 561 return - 1; 562 } 563 updateToString(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)564 private String updateToString(Uri uri, 565 ContentValues contentValues, 566 @Nullable String selection, 567 @Nullable String[] selectionArgs) { 568 return "Update { uri=" + uri + ", contentValues=" + contentValues + ", selection=" + 569 selection + ", selectionArgs" + Arrays.toString(selectionArgs) + '}'; 570 } 571 572 @Override delete(Uri uri, String selection, String[] selectionArgs)573 public int delete(Uri uri, String selection, String[] selectionArgs) { 574 if (mExpectedDeletes.isEmpty()) { 575 Assert.fail("Unexpected delete. Actual: " + deleteToString(uri, selection, 576 selectionArgs)); 577 } 578 for (Iterator<Delete> iterator = mExpectedDeletes.iterator(); iterator.hasNext(); ) { 579 Delete delete = iterator.next(); 580 if (delete.equals(uri, selection, selectionArgs)) { 581 delete.mIsExecuted = true; 582 if (!delete.mAnyNumberOfTimes) { 583 iterator.remove(); 584 } 585 return delete.mRowsAffected; 586 } 587 } 588 Assert.fail("Incorrect delete. Expected one of: " + mExpectedDeletes + ". Actual: " 589 + deleteToString(uri, selection, selectionArgs)); 590 return -1; 591 } 592 deleteToString(Uri uri, String selection, String[] selectionArgs)593 private String deleteToString(Uri uri, String selection, String[] selectionArgs) { 594 return "Delete { uri=" + uri + ", selection=" + selection + ", selectionArgs" 595 + Arrays.toString(selectionArgs) + '}'; 596 } 597 queryToString(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)598 private static String queryToString(Uri uri, String[] projection, String selection, 599 String[] selectionArgs, String sortOrder) { 600 StringBuilder sb = new StringBuilder(); 601 sb.append(uri).append(" "); 602 if (projection != null) { 603 sb.append(Arrays.toString(projection)); 604 } else { 605 sb.append("[]"); 606 } 607 if (selection != null) { 608 sb.append(" selection: '").append(selection).append("'"); 609 if (selectionArgs != null) { 610 sb.append(Arrays.toString(selectionArgs)); 611 } else { 612 sb.append("[]"); 613 } 614 } 615 if (sortOrder != null) { 616 sb.append(" sort: '").append(sortOrder).append("'"); 617 } 618 return sb.toString(); 619 } 620 verify()621 public void verify() { 622 verifyQueries(); 623 verifyInserts(); 624 verifyDeletes(); 625 } 626 verifyQueries()627 private void verifyQueries() { 628 List<Query> missedQueries = new ArrayList<>(); 629 for (Query query : mExpectedQueries) { 630 if (!query.mExecuted) { 631 missedQueries.add(query); 632 } 633 } 634 Assert.assertTrue("Not all expected queries have been called: " + missedQueries, 635 missedQueries.isEmpty()); 636 } 637 verifyInserts()638 private void verifyInserts() { 639 List<Insert> missedInserts = new ArrayList<>(); 640 for (Insert insert : mExpectedInserts) { 641 if (!insert.mIsExecuted) { 642 missedInserts.add(insert); 643 } 644 } 645 Assert.assertTrue("Not all expected inserts have been called: " + missedInserts, 646 missedInserts.isEmpty()); 647 } 648 verifyDeletes()649 private void verifyDeletes() { 650 List<Delete> missedDeletes = new ArrayList<>(); 651 for (Delete delete : mExpectedDeletes) { 652 if (!delete.mIsExecuted) { 653 missedDeletes.add(delete); 654 } 655 } 656 Assert.assertTrue("Not all expected deletes have been called: " + missedDeletes, 657 missedDeletes.isEmpty()); 658 } 659 } 660