1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/tests/background/junit3/src/sync/TestStoreTracking.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,377 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +package org.mozilla.gecko.background.sync; 1.8 + 1.9 +import java.util.concurrent.atomic.AtomicBoolean; 1.10 +import java.util.concurrent.atomic.AtomicLong; 1.11 + 1.12 +import junit.framework.AssertionFailedError; 1.13 + 1.14 +import org.mozilla.gecko.background.common.log.Logger; 1.15 +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; 1.16 +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate; 1.17 +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate; 1.18 +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate; 1.19 +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate; 1.20 +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate; 1.21 +import org.mozilla.gecko.background.testhelpers.WBORepository; 1.22 +import org.mozilla.gecko.sync.CryptoRecord; 1.23 +import org.mozilla.gecko.sync.ExtendedJSONObject; 1.24 +import org.mozilla.gecko.sync.repositories.InactiveSessionException; 1.25 +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; 1.26 +import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; 1.27 +import org.mozilla.gecko.sync.repositories.RepositorySession; 1.28 +import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; 1.29 +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; 1.30 +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; 1.31 +import org.mozilla.gecko.sync.repositories.domain.Record; 1.32 +import org.mozilla.gecko.sync.synchronizer.Synchronizer; 1.33 +import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate; 1.34 + 1.35 +import android.content.Context; 1.36 + 1.37 +public class TestStoreTracking extends AndroidSyncTestCase { 1.38 + public void assertEq(Object expected, Object actual) { 1.39 + try { 1.40 + assertEquals(expected, actual); 1.41 + } catch (AssertionFailedError e) { 1.42 + performNotify(e); 1.43 + } 1.44 + } 1.45 + 1.46 + public class TrackingWBORepository extends WBORepository { 1.47 + @Override 1.48 + public synchronized boolean shouldTrack() { 1.49 + return true; 1.50 + } 1.51 + } 1.52 + 1.53 + public void doTestStoreRetrieveByGUID(final WBORepository repository, 1.54 + final RepositorySession session, 1.55 + final String expectedGUID, 1.56 + final Record record) { 1.57 + 1.58 + final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() { 1.59 + 1.60 + @Override 1.61 + public void onRecordStoreSucceeded(String guid) { 1.62 + Logger.debug(getName(), "Stored " + guid); 1.63 + assertEq(expectedGUID, guid); 1.64 + } 1.65 + 1.66 + @Override 1.67 + public void onStoreCompleted(long storeEnd) { 1.68 + Logger.debug(getName(), "Store completed at " + storeEnd + "."); 1.69 + try { 1.70 + session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() { 1.71 + @Override 1.72 + public void onFetchedRecord(Record record) { 1.73 + Logger.debug(getName(), "Hurrah! Fetched record " + record.guid); 1.74 + assertEq(expectedGUID, record.guid); 1.75 + } 1.76 + 1.77 + @Override 1.78 + public void onFetchCompleted(final long fetchEnd) { 1.79 + Logger.debug(getName(), "Fetch completed at " + fetchEnd + "."); 1.80 + 1.81 + // But fetching by time returns nothing. 1.82 + session.fetchSince(0, new SimpleSuccessFetchDelegate() { 1.83 + private AtomicBoolean fetched = new AtomicBoolean(false); 1.84 + 1.85 + @Override 1.86 + public void onFetchedRecord(Record record) { 1.87 + Logger.debug(getName(), "Fetched record " + record.guid); 1.88 + fetched.set(true); 1.89 + performNotify(new AssertionFailedError("Should have fetched no record!")); 1.90 + } 1.91 + 1.92 + @Override 1.93 + public void onFetchCompleted(final long fetchEnd) { 1.94 + if (fetched.get()) { 1.95 + Logger.debug(getName(), "Not finishing session: record retrieved."); 1.96 + return; 1.97 + } 1.98 + try { 1.99 + session.finish(new SimpleSuccessFinishDelegate() { 1.100 + @Override 1.101 + public void onFinishSucceeded(RepositorySession session, 1.102 + RepositorySessionBundle bundle) { 1.103 + performNotify(); 1.104 + } 1.105 + }); 1.106 + } catch (InactiveSessionException e) { 1.107 + performNotify(e); 1.108 + } 1.109 + } 1.110 + }); 1.111 + } 1.112 + }); 1.113 + } catch (InactiveSessionException e) { 1.114 + performNotify(e); 1.115 + } 1.116 + } 1.117 + }; 1.118 + 1.119 + session.setStoreDelegate(storeDelegate); 1.120 + try { 1.121 + Logger.debug(getName(), "Storing..."); 1.122 + session.store(record); 1.123 + session.storeDone(); 1.124 + } catch (NoStoreDelegateException e) { 1.125 + // Should not happen. 1.126 + } 1.127 + } 1.128 + 1.129 + private void doTestNewSessionRetrieveByTime(final WBORepository repository, 1.130 + final String expectedGUID) { 1.131 + final SimpleSuccessCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { 1.132 + @Override 1.133 + public void onSessionCreated(final RepositorySession session) { 1.134 + Logger.debug(getName(), "Session created."); 1.135 + try { 1.136 + session.begin(new SimpleSuccessBeginDelegate() { 1.137 + @Override 1.138 + public void onBeginSucceeded(final RepositorySession session) { 1.139 + // Now we get a result. 1.140 + session.fetchSince(0, new SimpleSuccessFetchDelegate() { 1.141 + 1.142 + @Override 1.143 + public void onFetchedRecord(Record record) { 1.144 + assertEq(expectedGUID, record.guid); 1.145 + } 1.146 + 1.147 + @Override 1.148 + public void onFetchCompleted(long end) { 1.149 + try { 1.150 + session.finish(new SimpleSuccessFinishDelegate() { 1.151 + @Override 1.152 + public void onFinishSucceeded(RepositorySession session, 1.153 + RepositorySessionBundle bundle) { 1.154 + // Hooray! 1.155 + performNotify(); 1.156 + } 1.157 + }); 1.158 + } catch (InactiveSessionException e) { 1.159 + performNotify(e); 1.160 + } 1.161 + } 1.162 + }); 1.163 + } 1.164 + }); 1.165 + } catch (InvalidSessionTransitionException e) { 1.166 + performNotify(e); 1.167 + } 1.168 + } 1.169 + }; 1.170 + Runnable create = new Runnable() { 1.171 + @Override 1.172 + public void run() { 1.173 + repository.createSession(createDelegate, getApplicationContext()); 1.174 + } 1.175 + }; 1.176 + 1.177 + performWait(create); 1.178 + } 1.179 + 1.180 + /** 1.181 + * Store a record in one session. Verify that fetching by GUID returns 1.182 + * the record. Verify that fetching by timestamp fails to return records. 1.183 + * Start a new session. Verify that fetching by timestamp returns the 1.184 + * stored record. 1.185 + * 1.186 + * Invokes doTestStoreRetrieveByGUID, doTestNewSessionRetrieveByTime. 1.187 + */ 1.188 + public void testStoreRetrieveByGUID() { 1.189 + Logger.debug(getName(), "Started."); 1.190 + final WBORepository r = new TrackingWBORepository(); 1.191 + final long now = System.currentTimeMillis(); 1.192 + final String expectedGUID = "abcdefghijkl"; 1.193 + final Record record = new BookmarkRecord(expectedGUID, "bookmarks", now , false); 1.194 + 1.195 + final RepositorySessionCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { 1.196 + @Override 1.197 + public void onSessionCreated(RepositorySession session) { 1.198 + Logger.debug(getName(), "Session created: " + session); 1.199 + try { 1.200 + session.begin(new SimpleSuccessBeginDelegate() { 1.201 + @Override 1.202 + public void onBeginSucceeded(final RepositorySession session) { 1.203 + doTestStoreRetrieveByGUID(r, session, expectedGUID, record); 1.204 + } 1.205 + }); 1.206 + } catch (InvalidSessionTransitionException e) { 1.207 + performNotify(e); 1.208 + } 1.209 + } 1.210 + }; 1.211 + 1.212 + final Context applicationContext = getApplicationContext(); 1.213 + 1.214 + // This has to happen on a new thread so that we 1.215 + // can wait for it! 1.216 + Runnable create = onThreadRunnable(new Runnable() { 1.217 + @Override 1.218 + public void run() { 1.219 + r.createSession(createDelegate, applicationContext); 1.220 + } 1.221 + }); 1.222 + 1.223 + Runnable retrieve = onThreadRunnable(new Runnable() { 1.224 + @Override 1.225 + public void run() { 1.226 + doTestNewSessionRetrieveByTime(r, expectedGUID); 1.227 + performNotify(); 1.228 + } 1.229 + }); 1.230 + 1.231 + performWait(create); 1.232 + performWait(retrieve); 1.233 + } 1.234 + 1.235 + private Runnable onThreadRunnable(final Runnable r) { 1.236 + return new Runnable() { 1.237 + @Override 1.238 + public void run() { 1.239 + new Thread(r).start(); 1.240 + } 1.241 + }; 1.242 + } 1.243 + 1.244 + 1.245 + public class CountingWBORepository extends TrackingWBORepository { 1.246 + public AtomicLong counter = new AtomicLong(0L); 1.247 + public class CountingWBORepositorySession extends WBORepositorySession { 1.248 + private static final String LOG_TAG = "CountingRepoSession"; 1.249 + 1.250 + public CountingWBORepositorySession(WBORepository repository) { 1.251 + super(repository); 1.252 + } 1.253 + 1.254 + @Override 1.255 + public void store(final Record record) throws NoStoreDelegateException { 1.256 + Logger.debug(LOG_TAG, "Counter now " + counter.incrementAndGet()); 1.257 + super.store(record); 1.258 + } 1.259 + } 1.260 + 1.261 + @Override 1.262 + public void createSession(RepositorySessionCreationDelegate delegate, 1.263 + Context context) { 1.264 + delegate.deferredCreationDelegate().onSessionCreated(new CountingWBORepositorySession(this)); 1.265 + } 1.266 + } 1.267 + 1.268 + public class TestRecord extends Record { 1.269 + public TestRecord(String guid, String collection, long lastModified, 1.270 + boolean deleted) { 1.271 + super(guid, collection, lastModified, deleted); 1.272 + } 1.273 + 1.274 + @Override 1.275 + public void initFromEnvelope(CryptoRecord payload) { 1.276 + return; 1.277 + } 1.278 + 1.279 + @Override 1.280 + public CryptoRecord getEnvelope() { 1.281 + return null; 1.282 + } 1.283 + 1.284 + @Override 1.285 + protected void populatePayload(ExtendedJSONObject payload) { 1.286 + } 1.287 + 1.288 + @Override 1.289 + protected void initFromPayload(ExtendedJSONObject payload) { 1.290 + } 1.291 + 1.292 + @Override 1.293 + public Record copyWithIDs(String guid, long androidID) { 1.294 + return new TestRecord(guid, this.collection, this.lastModified, this.deleted); 1.295 + } 1.296 + } 1.297 + 1.298 + /** 1.299 + * Create two repositories, syncing from one to the other. Ensure 1.300 + * that records stored from one aren't re-uploaded. 1.301 + */ 1.302 + public void testStoreBetweenRepositories() { 1.303 + final CountingWBORepository repoA = new CountingWBORepository(); // "Remote". First source. 1.304 + final CountingWBORepository repoB = new CountingWBORepository(); // "Local". First sink. 1.305 + long now = System.currentTimeMillis(); 1.306 + 1.307 + TestRecord recordA1 = new TestRecord("aacdefghiaaa", "coll", now - 30, false); 1.308 + TestRecord recordA2 = new TestRecord("aacdefghibbb", "coll", now - 20, false); 1.309 + TestRecord recordB1 = new TestRecord("aacdefghiaaa", "coll", now - 10, false); 1.310 + TestRecord recordB2 = new TestRecord("aacdefghibbb", "coll", now - 40, false); 1.311 + 1.312 + TestRecord recordA3 = new TestRecord("nncdefghibbb", "coll", now, false); 1.313 + TestRecord recordB3 = new TestRecord("nncdefghiaaa", "coll", now, false); 1.314 + 1.315 + // A1 and B1 are the same, but B's version is newer. We expect A1 to be downloaded 1.316 + // and B1 to be uploaded. 1.317 + // A2 and B2 are the same, but A's version is newer. We expect A2 to be downloaded 1.318 + // and B2 to not be uploaded. 1.319 + // Both A3 and B3 are new. We expect them to go in each direction. 1.320 + // Expected counts, then: 1.321 + // Repo A: B1 + B3 1.322 + // Repo B: A1 + A2 + A3 1.323 + repoB.wbos.put(recordB1.guid, recordB1); 1.324 + repoB.wbos.put(recordB2.guid, recordB2); 1.325 + repoB.wbos.put(recordB3.guid, recordB3); 1.326 + repoA.wbos.put(recordA1.guid, recordA1); 1.327 + repoA.wbos.put(recordA2.guid, recordA2); 1.328 + repoA.wbos.put(recordA3.guid, recordA3); 1.329 + 1.330 + final Synchronizer s = new Synchronizer(); 1.331 + s.repositoryA = repoA; 1.332 + s.repositoryB = repoB; 1.333 + 1.334 + Runnable r = new Runnable() { 1.335 + @Override 1.336 + public void run() { 1.337 + s.synchronize(getApplicationContext(), new SynchronizerDelegate() { 1.338 + 1.339 + @Override 1.340 + public void onSynchronized(Synchronizer synchronizer) { 1.341 + long countA = repoA.counter.get(); 1.342 + long countB = repoB.counter.get(); 1.343 + Logger.debug(getName(), "Counts: " + countA + ", " + countB); 1.344 + assertEq(2L, countA); 1.345 + assertEq(3L, countB); 1.346 + 1.347 + // Testing for store timestamp 'hack'. 1.348 + // We fetched from A first, and so its bundle timestamp will be the last 1.349 + // stored time. We fetched from B second, so its bundle timestamp will be 1.350 + // the last fetched time. 1.351 + final long timestampA = synchronizer.bundleA.getTimestamp(); 1.352 + final long timestampB = synchronizer.bundleB.getTimestamp(); 1.353 + Logger.debug(getName(), "Repo A timestamp: " + timestampA); 1.354 + Logger.debug(getName(), "Repo B timestamp: " + timestampB); 1.355 + Logger.debug(getName(), "Repo A fetch done: " + repoA.stats.fetchCompleted); 1.356 + Logger.debug(getName(), "Repo A store done: " + repoA.stats.storeCompleted); 1.357 + Logger.debug(getName(), "Repo B fetch done: " + repoB.stats.fetchCompleted); 1.358 + Logger.debug(getName(), "Repo B store done: " + repoB.stats.storeCompleted); 1.359 + 1.360 + assertTrue(timestampB <= timestampA); 1.361 + assertTrue(repoA.stats.fetchCompleted <= timestampA); 1.362 + assertTrue(repoA.stats.storeCompleted >= repoA.stats.fetchCompleted); 1.363 + assertEquals(repoA.stats.storeCompleted, timestampA); 1.364 + assertEquals(repoB.stats.fetchCompleted, timestampB); 1.365 + performNotify(); 1.366 + } 1.367 + 1.368 + @Override 1.369 + public void onSynchronizeFailed(Synchronizer synchronizer, 1.370 + Exception lastException, String reason) { 1.371 + Logger.debug(getName(), "Failed."); 1.372 + performNotify(new AssertionFailedError("Should not fail.")); 1.373 + } 1.374 + }); 1.375 + } 1.376 + }; 1.377 + 1.378 + performWait(onThreadRunnable(r)); 1.379 + } 1.380 +}