|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 * http://creativecommons.org/publicdomain/zero/1.0/ |
|
3 */ |
|
4 |
|
5 // Test behaviour of module to perform deferred save of data |
|
6 // files to disk |
|
7 |
|
8 "use strict"; |
|
9 |
|
10 const testFile = gProfD.clone(); |
|
11 testFile.append("DeferredSaveTest"); |
|
12 |
|
13 Components.utils.import("resource://gre/modules/Promise.jsm"); |
|
14 |
|
15 let DSContext = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {}); |
|
16 let DeferredSave = DSContext.DeferredSave; |
|
17 |
|
18 // Test wrapper to let us do promise/task based testing of DeferredSave |
|
19 function DeferredSaveTester(aDataProvider) { |
|
20 let tester = { |
|
21 // Deferred for the promise returned by the mock writeAtomic |
|
22 waDeferred: null, |
|
23 |
|
24 // The most recent data "written" by the mock OS.File.writeAtomic |
|
25 writtenData: undefined, |
|
26 |
|
27 dataToSave: "Data to save", |
|
28 |
|
29 save: (aData, aWriteHandler) => { |
|
30 tester.writeHandler = aWriteHandler || writer; |
|
31 tester.dataToSave = aData; |
|
32 return tester.saver.saveChanges(); |
|
33 }, |
|
34 |
|
35 flush: (aWriteHandler) => { |
|
36 tester.writeHandler = aWriteHandler || writer; |
|
37 return tester.saver.flush(); |
|
38 }, |
|
39 |
|
40 get lastError() { |
|
41 return tester.saver.lastError; |
|
42 } |
|
43 }; |
|
44 |
|
45 // Default write handler for most cases where the test case doesn't need |
|
46 // to do anything while the write is in progress; just completes the write |
|
47 // on the next event loop |
|
48 function writer(aTester) { |
|
49 do_print("default write callback"); |
|
50 let length = aTester.writtenData.length; |
|
51 do_execute_soon(() => aTester.waDeferred.resolve(length)); |
|
52 } |
|
53 |
|
54 if (!aDataProvider) |
|
55 aDataProvider = () => tester.dataToSave; |
|
56 |
|
57 tester.saver = new DeferredSave(testFile.path, aDataProvider); |
|
58 |
|
59 // Install a mock for OS.File.writeAtomic to let us control the async |
|
60 // behaviour of the promise |
|
61 DSContext.OS.File.writeAtomic = function mock_writeAtomic(aFile, aData, aOptions) { |
|
62 do_print("writeAtomic: " + aFile + " data: '" + aData + "', " + aOptions.toSource()); |
|
63 tester.writtenData = aData; |
|
64 tester.waDeferred = Promise.defer(); |
|
65 tester.writeHandler(tester); |
|
66 return tester.waDeferred.promise; |
|
67 }; |
|
68 |
|
69 return tester; |
|
70 }; |
|
71 |
|
72 /** |
|
73 * Install a mock nsITimer factory that triggers on the next spin of |
|
74 * the event loop after it is scheduled |
|
75 */ |
|
76 function setQuickMockTimer() { |
|
77 let quickTimer = { |
|
78 initWithCallback: function(aFunction, aDelay, aType) { |
|
79 do_print("Starting quick timer, delay = " + aDelay); |
|
80 do_execute_soon(aFunction); |
|
81 }, |
|
82 cancel: function() { |
|
83 do_throw("Attempted to cancel a quickMockTimer"); |
|
84 } |
|
85 }; |
|
86 DSContext.MakeTimer = () => { |
|
87 do_print("Creating quick timer"); |
|
88 return quickTimer; |
|
89 }; |
|
90 } |
|
91 |
|
92 /** |
|
93 * Install a mock nsITimer factory in DeferredSave.jsm, returning a promise that resolves |
|
94 * when the client code sets the timer. Test cases can use this to wait for client code to |
|
95 * be ready for a timer event, and then signal the event by calling mockTimer.callback(). |
|
96 * This could use some enhancement; clients can re-use the returned timer, |
|
97 * but with this implementation it's not possible for the test to wait for |
|
98 * a second call to initWithCallback() on the re-used timer. |
|
99 * @return Promise{mockTimer} that resolves when initWithCallback() |
|
100 * is called |
|
101 */ |
|
102 function setPromiseMockTimer() { |
|
103 let waiter = Promise.defer(); |
|
104 let mockTimer = { |
|
105 callback: null, |
|
106 delay: null, |
|
107 type: null, |
|
108 isCancelled: false, |
|
109 |
|
110 initWithCallback: function(aFunction, aDelay, aType) { |
|
111 do_print("Starting timer, delay = " + aDelay); |
|
112 this.callback = aFunction; |
|
113 this.delay = aDelay; |
|
114 this.type = aType; |
|
115 // cancelled timers can be re-used |
|
116 this.isCancelled = false; |
|
117 waiter.resolve(this); |
|
118 }, |
|
119 cancel: function() { |
|
120 do_print("Cancelled mock timer"); |
|
121 this.callback = null; |
|
122 this.delay = null; |
|
123 this.type = null; |
|
124 this.isCancelled = true; |
|
125 // If initWithCallback was never called, resolve to let tests check for cancel |
|
126 waiter.resolve(this); |
|
127 } |
|
128 }; |
|
129 DSContext.MakeTimer = () => { |
|
130 do_print("Creating mock timer"); |
|
131 return mockTimer; |
|
132 }; |
|
133 return waiter.promise; |
|
134 } |
|
135 |
|
136 /** |
|
137 * Return a Promise<null> that resolves after the specified number of milliseconds |
|
138 */ |
|
139 function delay(aDelayMS) { |
|
140 let deferred = Promise.defer(); |
|
141 do_timeout(aDelayMS, () => deferred.resolve(null)); |
|
142 return deferred.promise; |
|
143 } |
|
144 |
|
145 function run_test() { |
|
146 run_next_test(); |
|
147 } |
|
148 |
|
149 // Modify set data once, ask for save, make sure it saves cleanly |
|
150 add_task(function test_basic_save_succeeds() { |
|
151 setQuickMockTimer(); |
|
152 let tester = DeferredSaveTester(); |
|
153 let data = "Test 1 Data"; |
|
154 |
|
155 yield tester.save(data); |
|
156 do_check_eq(tester.writtenData, data); |
|
157 do_check_eq(1, tester.saver.totalSaves); |
|
158 }); |
|
159 |
|
160 // Two saves called during the same event loop, both with callbacks |
|
161 // Make sure we save only the second version of the data |
|
162 add_task(function test_two_saves() { |
|
163 setQuickMockTimer(); |
|
164 let tester = DeferredSaveTester(); |
|
165 let firstCallback_happened = false; |
|
166 let firstData = "Test first save"; |
|
167 let secondData = "Test second save"; |
|
168 |
|
169 // first save should not resolve until after the second one is called, |
|
170 // so we can't just yield this promise |
|
171 tester.save(firstData).then(count => { |
|
172 do_check_eq(secondData, tester.writtenData); |
|
173 do_check_false(firstCallback_happened); |
|
174 firstCallback_happened = true; |
|
175 }, do_report_unexpected_exception); |
|
176 |
|
177 yield tester.save(secondData); |
|
178 do_check_true(firstCallback_happened); |
|
179 do_check_eq(secondData, tester.writtenData); |
|
180 do_check_eq(1, tester.saver.totalSaves); |
|
181 }); |
|
182 |
|
183 // Two saves called with a delay in between, both with callbacks |
|
184 // Make sure we save the second version of the data |
|
185 add_task(function test_two_saves_delay() { |
|
186 let timerPromise = setPromiseMockTimer(); |
|
187 let tester = DeferredSaveTester(); |
|
188 let firstCallback_happened = false; |
|
189 let delayDone = false; |
|
190 |
|
191 let firstData = "First data to save with delay"; |
|
192 let secondData = "Modified data to save with delay"; |
|
193 |
|
194 tester.save(firstData).then(count => { |
|
195 do_check_false(firstCallback_happened); |
|
196 do_check_true(delayDone); |
|
197 do_check_eq(secondData, tester.writtenData); |
|
198 firstCallback_happened = true; |
|
199 }, do_report_unexpected_exception); |
|
200 |
|
201 // Wait a short time to let async events possibly spawned by the |
|
202 // first tester.save() to run |
|
203 yield delay(2); |
|
204 delayDone = true; |
|
205 // request to save modified data |
|
206 let saving = tester.save(secondData); |
|
207 // Yield to wait for client code to set the timer |
|
208 let activeTimer = yield timerPromise; |
|
209 // and then trigger it |
|
210 activeTimer.callback(); |
|
211 // now wait for the DeferredSave to finish saving |
|
212 yield saving; |
|
213 do_check_true(firstCallback_happened); |
|
214 do_check_eq(secondData, tester.writtenData); |
|
215 do_check_eq(1, tester.saver.totalSaves); |
|
216 do_check_eq(0, tester.saver.overlappedSaves); |
|
217 }); |
|
218 |
|
219 // Test case where OS.File immediately reports an error when the write begins |
|
220 // Also check that the "error" getter correctly returns the error |
|
221 // Then do a write that succeeds, and make sure the error is cleared |
|
222 add_task(function test_error_immediate() { |
|
223 let tester = DeferredSaveTester(); |
|
224 let testError = new Error("Forced failure"); |
|
225 function writeFail(aTester) { |
|
226 aTester.waDeferred.reject(testError); |
|
227 } |
|
228 |
|
229 setQuickMockTimer(); |
|
230 yield tester.save("test_error_immediate", writeFail).then( |
|
231 count => do_throw("Did not get expected error"), |
|
232 error => do_check_eq(testError.message, error.message) |
|
233 ); |
|
234 do_check_eq(testError, tester.lastError); |
|
235 |
|
236 // This write should succeed and clear the error |
|
237 yield tester.save("test_error_immediate succeeds"); |
|
238 do_check_eq(null, tester.lastError); |
|
239 // The failed save attempt counts in our total |
|
240 do_check_eq(2, tester.saver.totalSaves); |
|
241 }); |
|
242 |
|
243 // Save one set of changes, then while the write is in progress, modify the |
|
244 // data two more times. Test that we re-write the dirty data exactly once |
|
245 // after the first write succeeds |
|
246 add_task(function dirty_while_writing() { |
|
247 let tester = DeferredSaveTester(); |
|
248 let firstData = "First data"; |
|
249 let secondData = "Second data"; |
|
250 let thirdData = "Third data"; |
|
251 let firstCallback_happened = false; |
|
252 let secondCallback_happened = false; |
|
253 let writeStarted = Promise.defer(); |
|
254 |
|
255 function writeCallback(aTester) { |
|
256 writeStarted.resolve(aTester.waDeferred); |
|
257 } |
|
258 |
|
259 setQuickMockTimer(); |
|
260 do_print("First save"); |
|
261 tester.save(firstData, writeCallback).then( |
|
262 count => { |
|
263 do_check_false(firstCallback_happened); |
|
264 do_check_false(secondCallback_happened); |
|
265 do_check_eq(tester.writtenData, firstData); |
|
266 firstCallback_happened = true; |
|
267 }, do_report_unexpected_exception); |
|
268 |
|
269 do_print("waiting for writer"); |
|
270 let writer = yield writeStarted.promise; |
|
271 do_print("Write started"); |
|
272 |
|
273 // Delay a bit, modify the data and call saveChanges, delay a bit more, |
|
274 // modify the data and call saveChanges again, another delay, |
|
275 // then complete the in-progress write |
|
276 yield delay(1); |
|
277 |
|
278 tester.save(secondData).then( |
|
279 count => { |
|
280 do_check_true(firstCallback_happened); |
|
281 do_check_false(secondCallback_happened); |
|
282 do_check_eq(tester.writtenData, thirdData); |
|
283 secondCallback_happened = true; |
|
284 }, do_report_unexpected_exception); |
|
285 |
|
286 // wait and then do the third change |
|
287 yield delay(1); |
|
288 let thirdWrite = tester.save(thirdData); |
|
289 |
|
290 // wait a bit more and then finally finish the first write |
|
291 yield delay(1); |
|
292 writer.resolve(firstData.length); |
|
293 |
|
294 // Now let everything else finish |
|
295 yield thirdWrite; |
|
296 do_check_true(firstCallback_happened); |
|
297 do_check_true(secondCallback_happened); |
|
298 do_check_eq(tester.writtenData, thirdData); |
|
299 do_check_eq(2, tester.saver.totalSaves); |
|
300 do_check_eq(1, tester.saver.overlappedSaves); |
|
301 }); |
|
302 |
|
303 // A write callback for the OS.File.writeAtomic mock that rejects write attempts |
|
304 function disabled_write_callback(aTester) { |
|
305 do_throw("Should not have written during clean flush"); |
|
306 deferred.reject(new Error("Write during supposedly clean flush")); |
|
307 } |
|
308 |
|
309 // special write callback that disables itself to make sure |
|
310 // we don't try to write twice |
|
311 function write_then_disable(aTester) { |
|
312 do_print("write_then_disable"); |
|
313 let length = aTester.writtenData.length; |
|
314 aTester.writeHandler = disabled_write_callback; |
|
315 do_execute_soon(() => aTester.waDeferred.resolve(length)); |
|
316 } |
|
317 |
|
318 // Flush tests. First, do an ordinary clean save and then call flush; |
|
319 // there should not be another save |
|
320 add_task(function flush_after_save() { |
|
321 setQuickMockTimer(); |
|
322 let tester = DeferredSaveTester(); |
|
323 let dataToSave = "Flush after save"; |
|
324 |
|
325 yield tester.save(dataToSave); |
|
326 yield tester.flush(disabled_write_callback); |
|
327 do_check_eq(1, tester.saver.totalSaves); |
|
328 }); |
|
329 |
|
330 // Flush while a write is in progress, but the in-memory data is clean |
|
331 add_task(function flush_during_write() { |
|
332 let tester = DeferredSaveTester(); |
|
333 let dataToSave = "Flush during write"; |
|
334 let firstCallback_happened = false; |
|
335 let writeStarted = Promise.defer(); |
|
336 |
|
337 function writeCallback(aTester) { |
|
338 writeStarted.resolve(aTester.waDeferred); |
|
339 } |
|
340 |
|
341 setQuickMockTimer(); |
|
342 tester.save(dataToSave, writeCallback).then( |
|
343 count => { |
|
344 do_check_false(firstCallback_happened); |
|
345 firstCallback_happened = true; |
|
346 }, do_report_unexpected_exception); |
|
347 |
|
348 let writer = yield writeStarted.promise; |
|
349 |
|
350 // call flush with the write callback disabled, delay a bit more, complete in-progress write |
|
351 let flushing = tester.flush(disabled_write_callback); |
|
352 yield delay(2); |
|
353 writer.resolve(dataToSave.length); |
|
354 |
|
355 // now wait for the flush to finish |
|
356 yield flushing; |
|
357 do_check_true(firstCallback_happened); |
|
358 do_check_eq(1, tester.saver.totalSaves); |
|
359 }); |
|
360 |
|
361 // Flush while dirty but write not in progress |
|
362 // The data written should be the value at the time |
|
363 // flush() is called, even if it is changed later |
|
364 add_task(function flush_while_dirty() { |
|
365 let timerPromise = setPromiseMockTimer(); |
|
366 let tester = DeferredSaveTester(); |
|
367 let firstData = "Flush while dirty, valid data"; |
|
368 let firstCallback_happened = false; |
|
369 |
|
370 tester.save(firstData, write_then_disable).then( |
|
371 count => { |
|
372 do_check_false(firstCallback_happened); |
|
373 firstCallback_happened = true; |
|
374 do_check_eq(tester.writtenData, firstData); |
|
375 }, do_report_unexpected_exception); |
|
376 |
|
377 // Wait for the timer to be set, but don't trigger it so the write won't start |
|
378 let activeTimer = yield timerPromise; |
|
379 |
|
380 let flushing = tester.flush(); |
|
381 |
|
382 // Make sure the timer was cancelled |
|
383 do_check_true(activeTimer.isCancelled); |
|
384 |
|
385 // Also make sure that data changed after the flush call |
|
386 // (even without a saveChanges() call) doesn't get written |
|
387 tester.dataToSave = "Flush while dirty, invalid data"; |
|
388 |
|
389 yield flushing; |
|
390 do_check_true(firstCallback_happened); |
|
391 do_check_eq(tester.writtenData, firstData); |
|
392 do_check_eq(1, tester.saver.totalSaves); |
|
393 }); |
|
394 |
|
395 // And the grand finale - modify the data, start writing, |
|
396 // modify the data again so we're in progress and dirty, |
|
397 // then flush, then modify the data again |
|
398 // Data for the second write should be taken at the time |
|
399 // flush() is called, even if it is modified later |
|
400 add_task(function flush_writing_dirty() { |
|
401 let timerPromise = setPromiseMockTimer(); |
|
402 let tester = DeferredSaveTester(); |
|
403 let firstData = "Flush first pass data"; |
|
404 let secondData = "Flush second pass data"; |
|
405 let firstCallback_happened = false; |
|
406 let secondCallback_happened = false; |
|
407 let writeStarted = Promise.defer(); |
|
408 |
|
409 function writeCallback(aTester) { |
|
410 writeStarted.resolve(aTester.waDeferred); |
|
411 } |
|
412 |
|
413 tester.save(firstData, writeCallback).then( |
|
414 count => { |
|
415 do_check_false(firstCallback_happened); |
|
416 do_check_eq(tester.writtenData, firstData); |
|
417 firstCallback_happened = true; |
|
418 }, do_report_unexpected_exception); |
|
419 |
|
420 // Trigger the timer callback as soon as the DeferredSave sets it |
|
421 let activeTimer = yield timerPromise; |
|
422 activeTimer.callback(); |
|
423 let writer = yield writeStarted.promise; |
|
424 // the first write has started |
|
425 |
|
426 // dirty the data and request another save |
|
427 // after the second save completes, there should not be another write |
|
428 tester.save(secondData, write_then_disable).then( |
|
429 count => { |
|
430 do_check_true(firstCallback_happened); |
|
431 do_check_false(secondCallback_happened); |
|
432 do_check_eq(tester.writtenData, secondData); |
|
433 secondCallback_happened = true; |
|
434 }, do_report_unexpected_exception); |
|
435 |
|
436 let flushing = tester.flush(write_then_disable); |
|
437 // Flush should have cancelled our timer |
|
438 do_check_true(activeTimer.isCancelled); |
|
439 tester.dataToSave = "Flush, invalid data: changed late"; |
|
440 // complete the first write |
|
441 writer.resolve(firstData.length); |
|
442 // now wait for the second write / flush to complete |
|
443 yield flushing; |
|
444 do_check_true(firstCallback_happened); |
|
445 do_check_true(secondCallback_happened); |
|
446 do_check_eq(tester.writtenData, secondData); |
|
447 do_check_eq(2, tester.saver.totalSaves); |
|
448 do_check_eq(1, tester.saver.overlappedSaves); |
|
449 }); |
|
450 |
|
451 // A data provider callback that throws an error the first |
|
452 // time it is called, and a different error the second time |
|
453 // so that tests can (a) make sure the promise is rejected |
|
454 // with the error and (b) make sure the provider is only |
|
455 // called once in case of error |
|
456 const expectedDataError = "Failed to serialize data"; |
|
457 let badDataError = null; |
|
458 function badDataProvider() { |
|
459 let err = new Error(badDataError); |
|
460 badDataError = "badDataProvider called twice"; |
|
461 throw err; |
|
462 } |
|
463 |
|
464 // Handle cases where data provider throws |
|
465 // First, throws during a normal save |
|
466 add_task(function data_throw() { |
|
467 setQuickMockTimer(); |
|
468 badDataError = expectedDataError; |
|
469 let tester = DeferredSaveTester(badDataProvider); |
|
470 yield tester.save("data_throw").then( |
|
471 count => do_throw("Expected serialization failure"), |
|
472 error => do_check_eq(error.message, expectedDataError)); |
|
473 }); |
|
474 |
|
475 // Now, throws during flush |
|
476 add_task(function data_throw_during_flush() { |
|
477 badDataError = expectedDataError; |
|
478 let tester = DeferredSaveTester(badDataProvider); |
|
479 let firstCallback_happened = false; |
|
480 |
|
481 setPromiseMockTimer(); |
|
482 // Write callback should never be called |
|
483 tester.save("data_throw_during_flush", disabled_write_callback).then( |
|
484 count => do_throw("Expected serialization failure"), |
|
485 error => { |
|
486 do_check_false(firstCallback_happened); |
|
487 do_check_eq(error.message, expectedDataError); |
|
488 firstCallback_happened = true; |
|
489 }); |
|
490 |
|
491 // flush() will cancel the timer |
|
492 yield tester.flush(disabled_write_callback).then( |
|
493 count => do_throw("Expected serialization failure"), |
|
494 error => do_check_eq(error.message, expectedDataError) |
|
495 ); |
|
496 |
|
497 do_check_true(firstCallback_happened); |
|
498 }); |
|
499 |
|
500 // Try to reproduce race condition. The observed sequence of events: |
|
501 // saveChanges |
|
502 // start writing |
|
503 // saveChanges |
|
504 // finish writing (need to restart delayed timer) |
|
505 // saveChanges |
|
506 // flush |
|
507 // write starts |
|
508 // actually restart timer for delayed write |
|
509 // write completes |
|
510 // delayed timer goes off, throws error because DeferredSave has been torn down |
|
511 add_task(function delay_flush_race() { |
|
512 let timerPromise = setPromiseMockTimer(); |
|
513 let tester = DeferredSaveTester(); |
|
514 let firstData = "First save"; |
|
515 let secondData = "Second save"; |
|
516 let thirdData = "Third save"; |
|
517 let writeStarted = Promise.defer(); |
|
518 |
|
519 function writeCallback(aTester) { |
|
520 writeStarted.resolve(aTester.waDeferred); |
|
521 } |
|
522 |
|
523 // This promise won't resolve until after writeStarted |
|
524 let firstSave = tester.save(firstData, writeCallback); |
|
525 (yield timerPromise).callback(); |
|
526 |
|
527 let writer = yield writeStarted.promise; |
|
528 // the first write has started |
|
529 |
|
530 // dirty the data and request another save |
|
531 let secondSave = tester.save(secondData); |
|
532 |
|
533 // complete the first write |
|
534 writer.resolve(firstData.length); |
|
535 yield firstSave; |
|
536 do_check_eq(tester.writtenData, firstData); |
|
537 |
|
538 tester.save(thirdData); |
|
539 let flushing = tester.flush(); |
|
540 |
|
541 yield secondSave; |
|
542 do_check_eq(tester.writtenData, thirdData); |
|
543 |
|
544 yield flushing; |
|
545 do_check_eq(tester.writtenData, thirdData); |
|
546 |
|
547 // Our DeferredSave should not have a _timer here; if it |
|
548 // does, the bug caused a reschedule |
|
549 do_check_eq(null, tester.saver._timer); |
|
550 }); |