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