michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: package org.mozilla.gecko.background.sync; michael@0: michael@0: import java.util.concurrent.atomic.AtomicBoolean; michael@0: import java.util.concurrent.atomic.AtomicLong; michael@0: michael@0: import junit.framework.AssertionFailedError; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; michael@0: import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate; michael@0: import org.mozilla.gecko.background.testhelpers.WBORepository; michael@0: import org.mozilla.gecko.sync.CryptoRecord; michael@0: import org.mozilla.gecko.sync.ExtendedJSONObject; michael@0: import org.mozilla.gecko.sync.repositories.InactiveSessionException; michael@0: import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; michael@0: import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; michael@0: import org.mozilla.gecko.sync.repositories.RepositorySession; michael@0: import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; michael@0: import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; michael@0: import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; michael@0: import org.mozilla.gecko.sync.repositories.domain.Record; michael@0: import org.mozilla.gecko.sync.synchronizer.Synchronizer; michael@0: import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate; michael@0: michael@0: import android.content.Context; michael@0: michael@0: public class TestStoreTracking extends AndroidSyncTestCase { michael@0: public void assertEq(Object expected, Object actual) { michael@0: try { michael@0: assertEquals(expected, actual); michael@0: } catch (AssertionFailedError e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: michael@0: public class TrackingWBORepository extends WBORepository { michael@0: @Override michael@0: public synchronized boolean shouldTrack() { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: public void doTestStoreRetrieveByGUID(final WBORepository repository, michael@0: final RepositorySession session, michael@0: final String expectedGUID, michael@0: final Record record) { michael@0: michael@0: final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() { michael@0: michael@0: @Override michael@0: public void onRecordStoreSucceeded(String guid) { michael@0: Logger.debug(getName(), "Stored " + guid); michael@0: assertEq(expectedGUID, guid); michael@0: } michael@0: michael@0: @Override michael@0: public void onStoreCompleted(long storeEnd) { michael@0: Logger.debug(getName(), "Store completed at " + storeEnd + "."); michael@0: try { michael@0: session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() { michael@0: @Override michael@0: public void onFetchedRecord(Record record) { michael@0: Logger.debug(getName(), "Hurrah! Fetched record " + record.guid); michael@0: assertEq(expectedGUID, record.guid); michael@0: } michael@0: michael@0: @Override michael@0: public void onFetchCompleted(final long fetchEnd) { michael@0: Logger.debug(getName(), "Fetch completed at " + fetchEnd + "."); michael@0: michael@0: // But fetching by time returns nothing. michael@0: session.fetchSince(0, new SimpleSuccessFetchDelegate() { michael@0: private AtomicBoolean fetched = new AtomicBoolean(false); michael@0: michael@0: @Override michael@0: public void onFetchedRecord(Record record) { michael@0: Logger.debug(getName(), "Fetched record " + record.guid); michael@0: fetched.set(true); michael@0: performNotify(new AssertionFailedError("Should have fetched no record!")); michael@0: } michael@0: michael@0: @Override michael@0: public void onFetchCompleted(final long fetchEnd) { michael@0: if (fetched.get()) { michael@0: Logger.debug(getName(), "Not finishing session: record retrieved."); michael@0: return; michael@0: } michael@0: try { michael@0: session.finish(new SimpleSuccessFinishDelegate() { michael@0: @Override michael@0: public void onFinishSucceeded(RepositorySession session, michael@0: RepositorySessionBundle bundle) { michael@0: performNotify(); michael@0: } michael@0: }); michael@0: } catch (InactiveSessionException e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: } catch (InactiveSessionException e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: session.setStoreDelegate(storeDelegate); michael@0: try { michael@0: Logger.debug(getName(), "Storing..."); michael@0: session.store(record); michael@0: session.storeDone(); michael@0: } catch (NoStoreDelegateException e) { michael@0: // Should not happen. michael@0: } michael@0: } michael@0: michael@0: private void doTestNewSessionRetrieveByTime(final WBORepository repository, michael@0: final String expectedGUID) { michael@0: final SimpleSuccessCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { michael@0: @Override michael@0: public void onSessionCreated(final RepositorySession session) { michael@0: Logger.debug(getName(), "Session created."); michael@0: try { michael@0: session.begin(new SimpleSuccessBeginDelegate() { michael@0: @Override michael@0: public void onBeginSucceeded(final RepositorySession session) { michael@0: // Now we get a result. michael@0: session.fetchSince(0, new SimpleSuccessFetchDelegate() { michael@0: michael@0: @Override michael@0: public void onFetchedRecord(Record record) { michael@0: assertEq(expectedGUID, record.guid); michael@0: } michael@0: michael@0: @Override michael@0: public void onFetchCompleted(long end) { michael@0: try { michael@0: session.finish(new SimpleSuccessFinishDelegate() { michael@0: @Override michael@0: public void onFinishSucceeded(RepositorySession session, michael@0: RepositorySessionBundle bundle) { michael@0: // Hooray! michael@0: performNotify(); michael@0: } michael@0: }); michael@0: } catch (InactiveSessionException e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: } catch (InvalidSessionTransitionException e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: }; michael@0: Runnable create = new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: repository.createSession(createDelegate, getApplicationContext()); michael@0: } michael@0: }; michael@0: michael@0: performWait(create); michael@0: } michael@0: michael@0: /** michael@0: * Store a record in one session. Verify that fetching by GUID returns michael@0: * the record. Verify that fetching by timestamp fails to return records. michael@0: * Start a new session. Verify that fetching by timestamp returns the michael@0: * stored record. michael@0: * michael@0: * Invokes doTestStoreRetrieveByGUID, doTestNewSessionRetrieveByTime. michael@0: */ michael@0: public void testStoreRetrieveByGUID() { michael@0: Logger.debug(getName(), "Started."); michael@0: final WBORepository r = new TrackingWBORepository(); michael@0: final long now = System.currentTimeMillis(); michael@0: final String expectedGUID = "abcdefghijkl"; michael@0: final Record record = new BookmarkRecord(expectedGUID, "bookmarks", now , false); michael@0: michael@0: final RepositorySessionCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { michael@0: @Override michael@0: public void onSessionCreated(RepositorySession session) { michael@0: Logger.debug(getName(), "Session created: " + session); michael@0: try { michael@0: session.begin(new SimpleSuccessBeginDelegate() { michael@0: @Override michael@0: public void onBeginSucceeded(final RepositorySession session) { michael@0: doTestStoreRetrieveByGUID(r, session, expectedGUID, record); michael@0: } michael@0: }); michael@0: } catch (InvalidSessionTransitionException e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: final Context applicationContext = getApplicationContext(); michael@0: michael@0: // This has to happen on a new thread so that we michael@0: // can wait for it! michael@0: Runnable create = onThreadRunnable(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: r.createSession(createDelegate, applicationContext); michael@0: } michael@0: }); michael@0: michael@0: Runnable retrieve = onThreadRunnable(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: doTestNewSessionRetrieveByTime(r, expectedGUID); michael@0: performNotify(); michael@0: } michael@0: }); michael@0: michael@0: performWait(create); michael@0: performWait(retrieve); michael@0: } michael@0: michael@0: private Runnable onThreadRunnable(final Runnable r) { michael@0: return new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: new Thread(r).start(); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: michael@0: public class CountingWBORepository extends TrackingWBORepository { michael@0: public AtomicLong counter = new AtomicLong(0L); michael@0: public class CountingWBORepositorySession extends WBORepositorySession { michael@0: private static final String LOG_TAG = "CountingRepoSession"; michael@0: michael@0: public CountingWBORepositorySession(WBORepository repository) { michael@0: super(repository); michael@0: } michael@0: michael@0: @Override michael@0: public void store(final Record record) throws NoStoreDelegateException { michael@0: Logger.debug(LOG_TAG, "Counter now " + counter.incrementAndGet()); michael@0: super.store(record); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void createSession(RepositorySessionCreationDelegate delegate, michael@0: Context context) { michael@0: delegate.deferredCreationDelegate().onSessionCreated(new CountingWBORepositorySession(this)); michael@0: } michael@0: } michael@0: michael@0: public class TestRecord extends Record { michael@0: public TestRecord(String guid, String collection, long lastModified, michael@0: boolean deleted) { michael@0: super(guid, collection, lastModified, deleted); michael@0: } michael@0: michael@0: @Override michael@0: public void initFromEnvelope(CryptoRecord payload) { michael@0: return; michael@0: } michael@0: michael@0: @Override michael@0: public CryptoRecord getEnvelope() { michael@0: return null; michael@0: } michael@0: michael@0: @Override michael@0: protected void populatePayload(ExtendedJSONObject payload) { michael@0: } michael@0: michael@0: @Override michael@0: protected void initFromPayload(ExtendedJSONObject payload) { michael@0: } michael@0: michael@0: @Override michael@0: public Record copyWithIDs(String guid, long androidID) { michael@0: return new TestRecord(guid, this.collection, this.lastModified, this.deleted); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Create two repositories, syncing from one to the other. Ensure michael@0: * that records stored from one aren't re-uploaded. michael@0: */ michael@0: public void testStoreBetweenRepositories() { michael@0: final CountingWBORepository repoA = new CountingWBORepository(); // "Remote". First source. michael@0: final CountingWBORepository repoB = new CountingWBORepository(); // "Local". First sink. michael@0: long now = System.currentTimeMillis(); michael@0: michael@0: TestRecord recordA1 = new TestRecord("aacdefghiaaa", "coll", now - 30, false); michael@0: TestRecord recordA2 = new TestRecord("aacdefghibbb", "coll", now - 20, false); michael@0: TestRecord recordB1 = new TestRecord("aacdefghiaaa", "coll", now - 10, false); michael@0: TestRecord recordB2 = new TestRecord("aacdefghibbb", "coll", now - 40, false); michael@0: michael@0: TestRecord recordA3 = new TestRecord("nncdefghibbb", "coll", now, false); michael@0: TestRecord recordB3 = new TestRecord("nncdefghiaaa", "coll", now, false); michael@0: michael@0: // A1 and B1 are the same, but B's version is newer. We expect A1 to be downloaded michael@0: // and B1 to be uploaded. michael@0: // A2 and B2 are the same, but A's version is newer. We expect A2 to be downloaded michael@0: // and B2 to not be uploaded. michael@0: // Both A3 and B3 are new. We expect them to go in each direction. michael@0: // Expected counts, then: michael@0: // Repo A: B1 + B3 michael@0: // Repo B: A1 + A2 + A3 michael@0: repoB.wbos.put(recordB1.guid, recordB1); michael@0: repoB.wbos.put(recordB2.guid, recordB2); michael@0: repoB.wbos.put(recordB3.guid, recordB3); michael@0: repoA.wbos.put(recordA1.guid, recordA1); michael@0: repoA.wbos.put(recordA2.guid, recordA2); michael@0: repoA.wbos.put(recordA3.guid, recordA3); michael@0: michael@0: final Synchronizer s = new Synchronizer(); michael@0: s.repositoryA = repoA; michael@0: s.repositoryB = repoB; michael@0: michael@0: Runnable r = new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: s.synchronize(getApplicationContext(), new SynchronizerDelegate() { michael@0: michael@0: @Override michael@0: public void onSynchronized(Synchronizer synchronizer) { michael@0: long countA = repoA.counter.get(); michael@0: long countB = repoB.counter.get(); michael@0: Logger.debug(getName(), "Counts: " + countA + ", " + countB); michael@0: assertEq(2L, countA); michael@0: assertEq(3L, countB); michael@0: michael@0: // Testing for store timestamp 'hack'. michael@0: // We fetched from A first, and so its bundle timestamp will be the last michael@0: // stored time. We fetched from B second, so its bundle timestamp will be michael@0: // the last fetched time. michael@0: final long timestampA = synchronizer.bundleA.getTimestamp(); michael@0: final long timestampB = synchronizer.bundleB.getTimestamp(); michael@0: Logger.debug(getName(), "Repo A timestamp: " + timestampA); michael@0: Logger.debug(getName(), "Repo B timestamp: " + timestampB); michael@0: Logger.debug(getName(), "Repo A fetch done: " + repoA.stats.fetchCompleted); michael@0: Logger.debug(getName(), "Repo A store done: " + repoA.stats.storeCompleted); michael@0: Logger.debug(getName(), "Repo B fetch done: " + repoB.stats.fetchCompleted); michael@0: Logger.debug(getName(), "Repo B store done: " + repoB.stats.storeCompleted); michael@0: michael@0: assertTrue(timestampB <= timestampA); michael@0: assertTrue(repoA.stats.fetchCompleted <= timestampA); michael@0: assertTrue(repoA.stats.storeCompleted >= repoA.stats.fetchCompleted); michael@0: assertEquals(repoA.stats.storeCompleted, timestampA); michael@0: assertEquals(repoB.stats.fetchCompleted, timestampB); michael@0: performNotify(); michael@0: } michael@0: michael@0: @Override michael@0: public void onSynchronizeFailed(Synchronizer synchronizer, michael@0: Exception lastException, String reason) { michael@0: Logger.debug(getName(), "Failed."); michael@0: performNotify(new AssertionFailedError("Should not fail.")); michael@0: } michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: performWait(onThreadRunnable(r)); michael@0: } michael@0: }