|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 package org.mozilla.gecko.background.sync; |
|
5 |
|
6 import java.util.concurrent.atomic.AtomicBoolean; |
|
7 import java.util.concurrent.atomic.AtomicLong; |
|
8 |
|
9 import junit.framework.AssertionFailedError; |
|
10 |
|
11 import org.mozilla.gecko.background.common.log.Logger; |
|
12 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; |
|
13 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate; |
|
14 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate; |
|
15 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate; |
|
16 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate; |
|
17 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate; |
|
18 import org.mozilla.gecko.background.testhelpers.WBORepository; |
|
19 import org.mozilla.gecko.sync.CryptoRecord; |
|
20 import org.mozilla.gecko.sync.ExtendedJSONObject; |
|
21 import org.mozilla.gecko.sync.repositories.InactiveSessionException; |
|
22 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; |
|
23 import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; |
|
24 import org.mozilla.gecko.sync.repositories.RepositorySession; |
|
25 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; |
|
26 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; |
|
27 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; |
|
28 import org.mozilla.gecko.sync.repositories.domain.Record; |
|
29 import org.mozilla.gecko.sync.synchronizer.Synchronizer; |
|
30 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate; |
|
31 |
|
32 import android.content.Context; |
|
33 |
|
34 public class TestStoreTracking extends AndroidSyncTestCase { |
|
35 public void assertEq(Object expected, Object actual) { |
|
36 try { |
|
37 assertEquals(expected, actual); |
|
38 } catch (AssertionFailedError e) { |
|
39 performNotify(e); |
|
40 } |
|
41 } |
|
42 |
|
43 public class TrackingWBORepository extends WBORepository { |
|
44 @Override |
|
45 public synchronized boolean shouldTrack() { |
|
46 return true; |
|
47 } |
|
48 } |
|
49 |
|
50 public void doTestStoreRetrieveByGUID(final WBORepository repository, |
|
51 final RepositorySession session, |
|
52 final String expectedGUID, |
|
53 final Record record) { |
|
54 |
|
55 final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() { |
|
56 |
|
57 @Override |
|
58 public void onRecordStoreSucceeded(String guid) { |
|
59 Logger.debug(getName(), "Stored " + guid); |
|
60 assertEq(expectedGUID, guid); |
|
61 } |
|
62 |
|
63 @Override |
|
64 public void onStoreCompleted(long storeEnd) { |
|
65 Logger.debug(getName(), "Store completed at " + storeEnd + "."); |
|
66 try { |
|
67 session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() { |
|
68 @Override |
|
69 public void onFetchedRecord(Record record) { |
|
70 Logger.debug(getName(), "Hurrah! Fetched record " + record.guid); |
|
71 assertEq(expectedGUID, record.guid); |
|
72 } |
|
73 |
|
74 @Override |
|
75 public void onFetchCompleted(final long fetchEnd) { |
|
76 Logger.debug(getName(), "Fetch completed at " + fetchEnd + "."); |
|
77 |
|
78 // But fetching by time returns nothing. |
|
79 session.fetchSince(0, new SimpleSuccessFetchDelegate() { |
|
80 private AtomicBoolean fetched = new AtomicBoolean(false); |
|
81 |
|
82 @Override |
|
83 public void onFetchedRecord(Record record) { |
|
84 Logger.debug(getName(), "Fetched record " + record.guid); |
|
85 fetched.set(true); |
|
86 performNotify(new AssertionFailedError("Should have fetched no record!")); |
|
87 } |
|
88 |
|
89 @Override |
|
90 public void onFetchCompleted(final long fetchEnd) { |
|
91 if (fetched.get()) { |
|
92 Logger.debug(getName(), "Not finishing session: record retrieved."); |
|
93 return; |
|
94 } |
|
95 try { |
|
96 session.finish(new SimpleSuccessFinishDelegate() { |
|
97 @Override |
|
98 public void onFinishSucceeded(RepositorySession session, |
|
99 RepositorySessionBundle bundle) { |
|
100 performNotify(); |
|
101 } |
|
102 }); |
|
103 } catch (InactiveSessionException e) { |
|
104 performNotify(e); |
|
105 } |
|
106 } |
|
107 }); |
|
108 } |
|
109 }); |
|
110 } catch (InactiveSessionException e) { |
|
111 performNotify(e); |
|
112 } |
|
113 } |
|
114 }; |
|
115 |
|
116 session.setStoreDelegate(storeDelegate); |
|
117 try { |
|
118 Logger.debug(getName(), "Storing..."); |
|
119 session.store(record); |
|
120 session.storeDone(); |
|
121 } catch (NoStoreDelegateException e) { |
|
122 // Should not happen. |
|
123 } |
|
124 } |
|
125 |
|
126 private void doTestNewSessionRetrieveByTime(final WBORepository repository, |
|
127 final String expectedGUID) { |
|
128 final SimpleSuccessCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { |
|
129 @Override |
|
130 public void onSessionCreated(final RepositorySession session) { |
|
131 Logger.debug(getName(), "Session created."); |
|
132 try { |
|
133 session.begin(new SimpleSuccessBeginDelegate() { |
|
134 @Override |
|
135 public void onBeginSucceeded(final RepositorySession session) { |
|
136 // Now we get a result. |
|
137 session.fetchSince(0, new SimpleSuccessFetchDelegate() { |
|
138 |
|
139 @Override |
|
140 public void onFetchedRecord(Record record) { |
|
141 assertEq(expectedGUID, record.guid); |
|
142 } |
|
143 |
|
144 @Override |
|
145 public void onFetchCompleted(long end) { |
|
146 try { |
|
147 session.finish(new SimpleSuccessFinishDelegate() { |
|
148 @Override |
|
149 public void onFinishSucceeded(RepositorySession session, |
|
150 RepositorySessionBundle bundle) { |
|
151 // Hooray! |
|
152 performNotify(); |
|
153 } |
|
154 }); |
|
155 } catch (InactiveSessionException e) { |
|
156 performNotify(e); |
|
157 } |
|
158 } |
|
159 }); |
|
160 } |
|
161 }); |
|
162 } catch (InvalidSessionTransitionException e) { |
|
163 performNotify(e); |
|
164 } |
|
165 } |
|
166 }; |
|
167 Runnable create = new Runnable() { |
|
168 @Override |
|
169 public void run() { |
|
170 repository.createSession(createDelegate, getApplicationContext()); |
|
171 } |
|
172 }; |
|
173 |
|
174 performWait(create); |
|
175 } |
|
176 |
|
177 /** |
|
178 * Store a record in one session. Verify that fetching by GUID returns |
|
179 * the record. Verify that fetching by timestamp fails to return records. |
|
180 * Start a new session. Verify that fetching by timestamp returns the |
|
181 * stored record. |
|
182 * |
|
183 * Invokes doTestStoreRetrieveByGUID, doTestNewSessionRetrieveByTime. |
|
184 */ |
|
185 public void testStoreRetrieveByGUID() { |
|
186 Logger.debug(getName(), "Started."); |
|
187 final WBORepository r = new TrackingWBORepository(); |
|
188 final long now = System.currentTimeMillis(); |
|
189 final String expectedGUID = "abcdefghijkl"; |
|
190 final Record record = new BookmarkRecord(expectedGUID, "bookmarks", now , false); |
|
191 |
|
192 final RepositorySessionCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { |
|
193 @Override |
|
194 public void onSessionCreated(RepositorySession session) { |
|
195 Logger.debug(getName(), "Session created: " + session); |
|
196 try { |
|
197 session.begin(new SimpleSuccessBeginDelegate() { |
|
198 @Override |
|
199 public void onBeginSucceeded(final RepositorySession session) { |
|
200 doTestStoreRetrieveByGUID(r, session, expectedGUID, record); |
|
201 } |
|
202 }); |
|
203 } catch (InvalidSessionTransitionException e) { |
|
204 performNotify(e); |
|
205 } |
|
206 } |
|
207 }; |
|
208 |
|
209 final Context applicationContext = getApplicationContext(); |
|
210 |
|
211 // This has to happen on a new thread so that we |
|
212 // can wait for it! |
|
213 Runnable create = onThreadRunnable(new Runnable() { |
|
214 @Override |
|
215 public void run() { |
|
216 r.createSession(createDelegate, applicationContext); |
|
217 } |
|
218 }); |
|
219 |
|
220 Runnable retrieve = onThreadRunnable(new Runnable() { |
|
221 @Override |
|
222 public void run() { |
|
223 doTestNewSessionRetrieveByTime(r, expectedGUID); |
|
224 performNotify(); |
|
225 } |
|
226 }); |
|
227 |
|
228 performWait(create); |
|
229 performWait(retrieve); |
|
230 } |
|
231 |
|
232 private Runnable onThreadRunnable(final Runnable r) { |
|
233 return new Runnable() { |
|
234 @Override |
|
235 public void run() { |
|
236 new Thread(r).start(); |
|
237 } |
|
238 }; |
|
239 } |
|
240 |
|
241 |
|
242 public class CountingWBORepository extends TrackingWBORepository { |
|
243 public AtomicLong counter = new AtomicLong(0L); |
|
244 public class CountingWBORepositorySession extends WBORepositorySession { |
|
245 private static final String LOG_TAG = "CountingRepoSession"; |
|
246 |
|
247 public CountingWBORepositorySession(WBORepository repository) { |
|
248 super(repository); |
|
249 } |
|
250 |
|
251 @Override |
|
252 public void store(final Record record) throws NoStoreDelegateException { |
|
253 Logger.debug(LOG_TAG, "Counter now " + counter.incrementAndGet()); |
|
254 super.store(record); |
|
255 } |
|
256 } |
|
257 |
|
258 @Override |
|
259 public void createSession(RepositorySessionCreationDelegate delegate, |
|
260 Context context) { |
|
261 delegate.deferredCreationDelegate().onSessionCreated(new CountingWBORepositorySession(this)); |
|
262 } |
|
263 } |
|
264 |
|
265 public class TestRecord extends Record { |
|
266 public TestRecord(String guid, String collection, long lastModified, |
|
267 boolean deleted) { |
|
268 super(guid, collection, lastModified, deleted); |
|
269 } |
|
270 |
|
271 @Override |
|
272 public void initFromEnvelope(CryptoRecord payload) { |
|
273 return; |
|
274 } |
|
275 |
|
276 @Override |
|
277 public CryptoRecord getEnvelope() { |
|
278 return null; |
|
279 } |
|
280 |
|
281 @Override |
|
282 protected void populatePayload(ExtendedJSONObject payload) { |
|
283 } |
|
284 |
|
285 @Override |
|
286 protected void initFromPayload(ExtendedJSONObject payload) { |
|
287 } |
|
288 |
|
289 @Override |
|
290 public Record copyWithIDs(String guid, long androidID) { |
|
291 return new TestRecord(guid, this.collection, this.lastModified, this.deleted); |
|
292 } |
|
293 } |
|
294 |
|
295 /** |
|
296 * Create two repositories, syncing from one to the other. Ensure |
|
297 * that records stored from one aren't re-uploaded. |
|
298 */ |
|
299 public void testStoreBetweenRepositories() { |
|
300 final CountingWBORepository repoA = new CountingWBORepository(); // "Remote". First source. |
|
301 final CountingWBORepository repoB = new CountingWBORepository(); // "Local". First sink. |
|
302 long now = System.currentTimeMillis(); |
|
303 |
|
304 TestRecord recordA1 = new TestRecord("aacdefghiaaa", "coll", now - 30, false); |
|
305 TestRecord recordA2 = new TestRecord("aacdefghibbb", "coll", now - 20, false); |
|
306 TestRecord recordB1 = new TestRecord("aacdefghiaaa", "coll", now - 10, false); |
|
307 TestRecord recordB2 = new TestRecord("aacdefghibbb", "coll", now - 40, false); |
|
308 |
|
309 TestRecord recordA3 = new TestRecord("nncdefghibbb", "coll", now, false); |
|
310 TestRecord recordB3 = new TestRecord("nncdefghiaaa", "coll", now, false); |
|
311 |
|
312 // A1 and B1 are the same, but B's version is newer. We expect A1 to be downloaded |
|
313 // and B1 to be uploaded. |
|
314 // A2 and B2 are the same, but A's version is newer. We expect A2 to be downloaded |
|
315 // and B2 to not be uploaded. |
|
316 // Both A3 and B3 are new. We expect them to go in each direction. |
|
317 // Expected counts, then: |
|
318 // Repo A: B1 + B3 |
|
319 // Repo B: A1 + A2 + A3 |
|
320 repoB.wbos.put(recordB1.guid, recordB1); |
|
321 repoB.wbos.put(recordB2.guid, recordB2); |
|
322 repoB.wbos.put(recordB3.guid, recordB3); |
|
323 repoA.wbos.put(recordA1.guid, recordA1); |
|
324 repoA.wbos.put(recordA2.guid, recordA2); |
|
325 repoA.wbos.put(recordA3.guid, recordA3); |
|
326 |
|
327 final Synchronizer s = new Synchronizer(); |
|
328 s.repositoryA = repoA; |
|
329 s.repositoryB = repoB; |
|
330 |
|
331 Runnable r = new Runnable() { |
|
332 @Override |
|
333 public void run() { |
|
334 s.synchronize(getApplicationContext(), new SynchronizerDelegate() { |
|
335 |
|
336 @Override |
|
337 public void onSynchronized(Synchronizer synchronizer) { |
|
338 long countA = repoA.counter.get(); |
|
339 long countB = repoB.counter.get(); |
|
340 Logger.debug(getName(), "Counts: " + countA + ", " + countB); |
|
341 assertEq(2L, countA); |
|
342 assertEq(3L, countB); |
|
343 |
|
344 // Testing for store timestamp 'hack'. |
|
345 // We fetched from A first, and so its bundle timestamp will be the last |
|
346 // stored time. We fetched from B second, so its bundle timestamp will be |
|
347 // the last fetched time. |
|
348 final long timestampA = synchronizer.bundleA.getTimestamp(); |
|
349 final long timestampB = synchronizer.bundleB.getTimestamp(); |
|
350 Logger.debug(getName(), "Repo A timestamp: " + timestampA); |
|
351 Logger.debug(getName(), "Repo B timestamp: " + timestampB); |
|
352 Logger.debug(getName(), "Repo A fetch done: " + repoA.stats.fetchCompleted); |
|
353 Logger.debug(getName(), "Repo A store done: " + repoA.stats.storeCompleted); |
|
354 Logger.debug(getName(), "Repo B fetch done: " + repoB.stats.fetchCompleted); |
|
355 Logger.debug(getName(), "Repo B store done: " + repoB.stats.storeCompleted); |
|
356 |
|
357 assertTrue(timestampB <= timestampA); |
|
358 assertTrue(repoA.stats.fetchCompleted <= timestampA); |
|
359 assertTrue(repoA.stats.storeCompleted >= repoA.stats.fetchCompleted); |
|
360 assertEquals(repoA.stats.storeCompleted, timestampA); |
|
361 assertEquals(repoB.stats.fetchCompleted, timestampB); |
|
362 performNotify(); |
|
363 } |
|
364 |
|
365 @Override |
|
366 public void onSynchronizeFailed(Synchronizer synchronizer, |
|
367 Exception lastException, String reason) { |
|
368 Logger.debug(getName(), "Failed."); |
|
369 performNotify(new AssertionFailedError("Should not fail.")); |
|
370 } |
|
371 }); |
|
372 } |
|
373 }; |
|
374 |
|
375 performWait(onThreadRunnable(r)); |
|
376 } |
|
377 } |