blob: 42c1e789b9c31d48b9f899183847ba2214057f6d [file] [log] [blame]
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) {
}
}
}