| package android.content; |
| |
| import com.google.android.collect.Lists; |
| import com.google.android.collect.Sets; |
| |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.net.Uri; |
| import android.test.AndroidTestCase; |
| import android.text.TextUtils; |
| import android.accounts.Account; |
| |
| import java.util.ArrayList; |
| import java.util.Map; |
| import java.util.SortedSet; |
| |
| /** Unit test for {@link android.content.AbstractTableMerger}. */ |
| public class AbstractTableMergerTest extends AndroidTestCase { |
| MockSyncableContentProvider mRealProvider; |
| MockSyncableContentProvider mTempProvider; |
| MockTableMerger mMerger; |
| MockSyncContext mSyncContext; |
| |
| static final String TABLE_NAME = "items"; |
| static final String DELETED_TABLE_NAME = "deleted_items"; |
| static final Uri CONTENT_URI = Uri.parse("content://testdata"); |
| static final Uri TABLE_URI = Uri.withAppendedPath(CONTENT_URI, TABLE_NAME); |
| static final Uri DELETED_TABLE_URI = Uri.withAppendedPath(CONTENT_URI, DELETED_TABLE_NAME); |
| |
| private final Account ACCOUNT = new Account("account@goo.com", "example.type"); |
| |
| private final ArrayList<Expectation> mExpectations = Lists.newArrayList(); |
| |
| static class Expectation { |
| enum Type { |
| UPDATE, |
| INSERT, |
| DELETE, |
| RESOLVE |
| } |
| |
| Type mType; |
| ContentValues mValues; |
| Long mLocalRowId; |
| |
| Expectation(Type type, Long localRowId, ContentValues values) { |
| mType = type; |
| mValues = values; |
| mLocalRowId = localRowId; |
| if (type == Type.DELETE) { |
| assertNull(values); |
| } else { |
| assertFalse(values.containsKey("_id")); |
| } |
| } |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| mSyncContext = new MockSyncContext(); |
| mRealProvider = new MockSyncableContentProvider(); |
| mTempProvider = mRealProvider.getTemporaryInstance(); |
| mMerger = new MockTableMerger(mRealProvider.getDatabase(), |
| TABLE_NAME, TABLE_URI, DELETED_TABLE_NAME, DELETED_TABLE_URI); |
| mExpectations.clear(); |
| } |
| |
| ContentValues newValues(String data, String syncId, Account syncAccount, |
| String syncTime, String syncVersion, Long syncLocalId) { |
| ContentValues values = new ContentValues(); |
| if (data != null) values.put("data", data); |
| if (syncTime != null) values.put("_sync_time", syncTime); |
| if (syncVersion != null) values.put("_sync_version", syncVersion); |
| if (syncId != null) values.put("_sync_id", syncId); |
| if (syncAccount != null) { |
| values.put("_sync_account", syncAccount.mName); |
| values.put("_sync_account_type", syncAccount.mType); |
| } |
| values.put("_sync_local_id", syncLocalId); |
| values.put("_sync_dirty", 0); |
| return values; |
| } |
| |
| ContentValues newDeletedValues(String syncId, Account syncAccount, String syncVersion, |
| Long syncLocalId) { |
| ContentValues values = new ContentValues(); |
| if (syncVersion != null) values.put("_sync_version", syncVersion); |
| if (syncId != null) values.put("_sync_id", syncId); |
| if (syncAccount != null) { |
| values.put("_sync_account", syncAccount.mName); |
| values.put("_sync_account_type", syncAccount.mType); |
| } |
| if (syncLocalId != null) values.put("_sync_local_id", syncLocalId); |
| return values; |
| } |
| |
| ContentValues newModifyData(String data) { |
| ContentValues values = new ContentValues(); |
| values.put("data", data); |
| values.put("_sync_dirty", 1); |
| return values; |
| } |
| |
| // Want to test adding, changing, deleting entries to a provider that has extra entries |
| // before and after the entries being changed. |
| public void testInsert() { |
| // add rows to the real provider |
| // add new row to the temp provider |
| final ContentValues row1 = newValues("d1", "si1", ACCOUNT, "st1", "sv1", null); |
| mTempProvider.insert(TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.INSERT, null /* syncLocalId */, row1)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testUpdateWithLocalId() { |
| // add rows to the real provider |
| // add new row to the temp provider that matches an unsynced row in the real provider |
| final ContentValues row1 = newValues("d1", "si1", ACCOUNT, "st1", "sv1", 11L); |
| mTempProvider.insert(TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.UPDATE, 11L, row1)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testUpdateWithoutLocalId() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, |
| newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); |
| |
| // add new row to the temp provider that matches an unsynced row in the real provider |
| final ContentValues row1 = newValues("d2", "si1", ACCOUNT, "st2", "sv2", null); |
| mTempProvider.insert(TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.UPDATE, ContentUris.parseId(i1), row1)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testResolve() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, |
| newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); |
| mRealProvider.update(TABLE_URI, newModifyData("d2"), null, null); |
| |
| // add row to the temp provider that matches a dirty, synced row in the real provider |
| final ContentValues row1 = newValues("d3", "si1", ACCOUNT, "st2", "sv2", null); |
| mTempProvider.insert(TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.RESOLVE, ContentUris.parseId(i1), row1)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testResolveWithLocalId() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, |
| newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); |
| mRealProvider.update(TABLE_URI, newModifyData("d2"), null, null); |
| |
| // add row to the temp provider that matches a dirty, synced row in the real provider |
| ContentValues row1 = newValues("d2", "si1", ACCOUNT, "st2", "sv2", ContentUris.parseId(i1)); |
| mTempProvider.insert(TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.UPDATE, ContentUris.parseId(i1), row1)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testDeleteRowAfterDelete() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, |
| newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); |
| |
| // add a deleted record to the temp provider |
| ContentValues row1 = newDeletedValues(null, null, null, ContentUris.parseId(i1)); |
| mTempProvider.insert(DELETED_TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testDeleteRowAfterInsert() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, newModifyData("d1")); |
| |
| // add a deleted record to the temp provider |
| ContentValues row1 = newDeletedValues(null, null, null, ContentUris.parseId(i1)); |
| mTempProvider.insert(DELETED_TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testDeleteRowAfterUpdate() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, |
| newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); |
| |
| // add a deleted record to the temp provider |
| ContentValues row1 = newDeletedValues("si1", ACCOUNT, "sv1", ContentUris.parseId(i1)); |
| mTempProvider.insert(DELETED_TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| public void testDeleteRowFromServer() { |
| // add rows to the real provider |
| Uri i1 = mRealProvider.insert(TABLE_URI, |
| newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); |
| |
| // add a deleted record to the temp provider |
| ContentValues row1 = newDeletedValues("si1", ACCOUNT, "sv1", null); |
| mTempProvider.insert(DELETED_TABLE_URI, row1); |
| |
| // add expected callbacks to merger |
| mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); |
| |
| // run merger |
| SyncResult syncResult = new SyncResult(); |
| mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); |
| |
| // check that all expectations were met |
| assertEquals("not all expectations were met", 0, mExpectations.size()); |
| } |
| |
| class MockTableMerger extends AbstractTableMerger { |
| public MockTableMerger(SQLiteDatabase database, String table, Uri tableURL, |
| String deletedTable, Uri deletedTableURL) { |
| super(database, table, tableURL, deletedTable, deletedTableURL); |
| } |
| |
| public void insertRow(ContentProvider diffs, Cursor diffsCursor) { |
| Expectation expectation = mExpectations.remove(0); |
| checkExpectation(expectation, |
| Expectation.Type.INSERT, null /* syncLocalId */, diffsCursor); |
| } |
| |
| public void updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor) { |
| Expectation expectation = mExpectations.remove(0); |
| checkExpectation(expectation, Expectation.Type.UPDATE, localPersonID, diffsCursor); |
| } |
| |
| public void resolveRow(long localPersonID, String syncID, ContentProvider diffs, |
| Cursor diffsCursor) { |
| Expectation expectation = mExpectations.remove(0); |
| checkExpectation(expectation, Expectation.Type.RESOLVE, localPersonID, diffsCursor); |
| } |
| |
| @Override |
| public void deleteRow(Cursor cursor) { |
| Expectation expectation = mExpectations.remove(0); |
| assertEquals(expectation.mType, Expectation.Type.DELETE); |
| assertNotNull(expectation.mLocalRowId); |
| final long localRowId = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); |
| assertEquals((long)expectation.mLocalRowId, localRowId); |
| cursor.moveToNext(); |
| mDb.delete(TABLE_NAME, "_id=" + localRowId, null); |
| } |
| |
| protected void notifyChanges() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| void checkExpectation(Expectation expectation, |
| Expectation.Type actualType, Long localRowId, |
| Cursor cursor) { |
| assertEquals(expectation.mType, actualType); |
| assertEquals(expectation.mLocalRowId, localRowId); |
| |
| final SortedSet<String> actualKeys = Sets.newSortedSet(cursor.getColumnNames()); |
| final SortedSet<String> expectedKeys = Sets.newSortedSet(); |
| for (Map.Entry<String, Object> entry : expectation.mValues.valueSet()) { |
| expectedKeys.add(entry.getKey()); |
| } |
| actualKeys.remove("_id"); |
| actualKeys.remove("_sync_mark"); |
| actualKeys.remove("_sync_local_id"); |
| expectedKeys.remove("_sync_local_id"); |
| expectedKeys.remove("_id"); |
| assertEquals("column mismatch", |
| TextUtils.join(",", expectedKeys), TextUtils.join(",", actualKeys)); |
| |
| // if (localRowId != null) { |
| // assertEquals((long) localRowId, |
| // cursor.getLong(cursor.getColumnIndexOrThrow("_sync_local_id"))); |
| // } else { |
| // assertTrue("unexpected _sync_local_id, " |
| // + cursor.getLong(cursor.getColumnIndexOrThrow("_sync_local_id")), |
| // cursor.isNull(cursor.getColumnIndexOrThrow("_sync_local_id"))); |
| // } |
| |
| for (String name : cursor.getColumnNames()) { |
| if ("_id".equals(name)) { |
| continue; |
| } |
| if (cursor.isNull(cursor.getColumnIndexOrThrow(name))) { |
| assertNull(expectation.mValues.getAsString(name)); |
| } else { |
| String actualValue = |
| cursor.getString(cursor.getColumnIndexOrThrow(name)); |
| assertEquals("mismatch on column " + name, |
| expectation.mValues.getAsString(name), actualValue); |
| } |
| } |
| } |
| } |
| |
| class MockSyncableContentProvider extends SyncableContentProvider { |
| SQLiteDatabase mDb; |
| boolean mIsTemporary; |
| boolean mContainsDiffs; |
| |
| private final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
| |
| private static final int MATCHER_ITEMS = 0; |
| private static final int MATCHER_DELETED_ITEMS = 1; |
| |
| public MockSyncableContentProvider() { |
| mIsTemporary = false; |
| setContainsDiffs(false); |
| sURIMatcher.addURI(CONTENT_URI.getAuthority(), "items", MATCHER_ITEMS); |
| sURIMatcher.addURI(CONTENT_URI.getAuthority(), "deleted_items", MATCHER_DELETED_ITEMS); |
| |
| mDb = SQLiteDatabase.create(null); |
| mDb.execSQL("CREATE TABLE items (" |
| + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " |
| + "data TEXT, " |
| + "_sync_time TEXT, " |
| + "_sync_version TEXT, " |
| + "_sync_id TEXT, " |
| + "_sync_local_id INTEGER, " |
| + "_sync_dirty INTEGER NOT NULL DEFAULT 0, " |
| + "_sync_account TEXT, " |
| + "_sync_account_type TEXT, " |
| + "_sync_mark INTEGER)"); |
| |
| mDb.execSQL("CREATE TABLE deleted_items (" |
| + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " |
| + "_sync_version TEXT, " |
| + "_sync_id TEXT, " |
| + "_sync_local_id INTEGER, " |
| + "_sync_account TEXT, " |
| + "_sync_account_type TEXT, " |
| + "_sync_mark INTEGER)"); |
| } |
| |
| public boolean onCreate() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, |
| String sortOrder) { |
| int match = sURIMatcher.match(uri); |
| switch (match) { |
| case MATCHER_ITEMS: |
| return mDb.query(TABLE_NAME, projection, selection, selectionArgs, |
| null, null, sortOrder); |
| case MATCHER_DELETED_ITEMS: |
| return mDb.query(DELETED_TABLE_NAME, projection, selection, selectionArgs, |
| null, null, sortOrder); |
| default: |
| throw new UnsupportedOperationException("Cannot query URL: " + uri); |
| } |
| } |
| |
| public String getType(Uri uri) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public Uri insert(Uri uri, ContentValues values) { |
| int match = sURIMatcher.match(uri); |
| switch (match) { |
| case MATCHER_ITEMS: { |
| long id = mDb.insert(TABLE_NAME, "_id", values); |
| return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); |
| } |
| case MATCHER_DELETED_ITEMS: { |
| long id = mDb.insert(DELETED_TABLE_NAME, "_id", values); |
| return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); |
| } |
| default: |
| throw new UnsupportedOperationException("Cannot query URL: " + uri); |
| } |
| } |
| |
| public int delete(Uri uri, String selection, String[] selectionArgs) { |
| int match = sURIMatcher.match(uri); |
| switch (match) { |
| case MATCHER_ITEMS: |
| return mDb.delete(TABLE_NAME, selection, selectionArgs); |
| case MATCHER_DELETED_ITEMS: |
| return mDb.delete(DELETED_TABLE_NAME, selection, selectionArgs); |
| default: |
| throw new UnsupportedOperationException("Cannot query URL: " + uri); |
| } |
| } |
| |
| public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
| int match = sURIMatcher.match(uri); |
| switch (match) { |
| case MATCHER_ITEMS: |
| return mDb.update(TABLE_NAME, values, selection, selectionArgs); |
| case MATCHER_DELETED_ITEMS: |
| return mDb.update(DELETED_TABLE_NAME, values, selection, selectionArgs); |
| default: |
| throw new UnsupportedOperationException("Cannot query URL: " + uri); |
| } |
| } |
| |
| protected boolean isTemporary() { |
| return mIsTemporary; |
| } |
| |
| public void close() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected void bootstrapDatabase(SQLiteDatabase db) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected void onDatabaseOpened(SQLiteDatabase db) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public MockSyncableContentProvider getTemporaryInstance() { |
| MockSyncableContentProvider temp = new MockSyncableContentProvider(); |
| temp.mIsTemporary = true; |
| temp.setContainsDiffs(true); |
| return temp; |
| } |
| |
| public SQLiteDatabase getDatabase() { |
| return mDb; |
| } |
| |
| public boolean getContainsDiffs() { |
| return mContainsDiffs; |
| } |
| |
| public void setContainsDiffs(boolean containsDiffs) { |
| mContainsDiffs = containsDiffs; |
| } |
| |
| protected Iterable<? extends AbstractTableMerger> getMergers() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public boolean changeRequiresLocalSync(Uri uri) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void onSyncStart(SyncContext context, Account account) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void onSyncStop(SyncContext context, boolean success) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public Account getSyncingAccount() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void merge(SyncContext context, SyncableContentProvider diffs, |
| TempProviderSyncResult result, SyncResult syncResult) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void onSyncCanceled() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public boolean isMergeCancelled() { |
| return false; |
| } |
| |
| protected int updateInternal(Uri url, ContentValues values, String selection, |
| String[] selectionArgs) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected int deleteInternal(Uri url, String selection, String[] selectionArgs) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected Uri insertInternal(Uri url, ContentValues values) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected Cursor queryInternal(Uri url, String[] projection, String selection, |
| String[] selectionArgs, String sortOrder) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected void onAccountsChanged(Account[] accountsArray) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, String table |
| ) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void wipeAccount(Account account) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public byte[] readSyncDataBytes(Account account) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void writeSyncDataBytes(Account account, byte[] data) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| class MockSyncContext extends SyncContext { |
| public MockSyncContext() { |
| super(null); |
| } |
| |
| @Override |
| public void setStatusText(String message) { |
| } |
| } |
| } |