/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ package org.mozilla.android.sync.test; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import junit.framework.AssertionFailedError; import org.mozilla.android.sync.test.helpers.WBORepository; import org.mozilla.android.sync.test.helpers.simple.SimpleSuccessBeginDelegate; import org.mozilla.android.sync.test.helpers.simple.SimpleSuccessCreationDelegate; import org.mozilla.android.sync.test.helpers.simple.SimpleSuccessFetchDelegate; import org.mozilla.android.sync.test.helpers.simple.SimpleSuccessFinishDelegate; import org.mozilla.android.sync.test.helpers.simple.SimpleSuccessStoreDelegate; import org.mozilla.gecko.sync.CryptoRecord; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.Logger; 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 StoreTrackingTest 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)); } }