Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* Any copyright is dedicated to the Public Domain.
2 * http://creativecommons.org/publicdomain/zero/1.0/
3 */
5 // Test behaviour of module to perform deferred save of data
6 // files to disk
8 "use strict";
10 const testFile = gProfD.clone();
11 testFile.append("DeferredSaveTest");
13 Components.utils.import("resource://gre/modules/Promise.jsm");
15 let DSContext = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {});
16 let DeferredSave = DSContext.DeferredSave;
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,
24 // The most recent data "written" by the mock OS.File.writeAtomic
25 writtenData: undefined,
27 dataToSave: "Data to save",
29 save: (aData, aWriteHandler) => {
30 tester.writeHandler = aWriteHandler || writer;
31 tester.dataToSave = aData;
32 return tester.saver.saveChanges();
33 },
35 flush: (aWriteHandler) => {
36 tester.writeHandler = aWriteHandler || writer;
37 return tester.saver.flush();
38 },
40 get lastError() {
41 return tester.saver.lastError;
42 }
43 };
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 }
54 if (!aDataProvider)
55 aDataProvider = () => tester.dataToSave;
57 tester.saver = new DeferredSave(testFile.path, aDataProvider);
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 };
69 return tester;
70 };
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 }
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,
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 }
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 }
145 function run_test() {
146 run_next_test();
147 }
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";
155 yield tester.save(data);
156 do_check_eq(tester.writtenData, data);
157 do_check_eq(1, tester.saver.totalSaves);
158 });
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";
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);
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 });
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;
191 let firstData = "First data to save with delay";
192 let secondData = "Modified data to save with delay";
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);
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 });
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 }
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);
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 });
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();
255 function writeCallback(aTester) {
256 writeStarted.resolve(aTester.waDeferred);
257 }
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);
269 do_print("waiting for writer");
270 let writer = yield writeStarted.promise;
271 do_print("Write started");
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);
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);
286 // wait and then do the third change
287 yield delay(1);
288 let thirdWrite = tester.save(thirdData);
290 // wait a bit more and then finally finish the first write
291 yield delay(1);
292 writer.resolve(firstData.length);
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 });
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 }
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 }
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";
325 yield tester.save(dataToSave);
326 yield tester.flush(disabled_write_callback);
327 do_check_eq(1, tester.saver.totalSaves);
328 });
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();
337 function writeCallback(aTester) {
338 writeStarted.resolve(aTester.waDeferred);
339 }
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);
348 let writer = yield writeStarted.promise;
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);
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 });
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;
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);
377 // Wait for the timer to be set, but don't trigger it so the write won't start
378 let activeTimer = yield timerPromise;
380 let flushing = tester.flush();
382 // Make sure the timer was cancelled
383 do_check_true(activeTimer.isCancelled);
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";
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 });
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();
409 function writeCallback(aTester) {
410 writeStarted.resolve(aTester.waDeferred);
411 }
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);
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
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);
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 });
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 }
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 });
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;
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 });
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 );
497 do_check_true(firstCallback_happened);
498 });
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();
519 function writeCallback(aTester) {
520 writeStarted.resolve(aTester.waDeferred);
521 }
523 // This promise won't resolve until after writeStarted
524 let firstSave = tester.save(firstData, writeCallback);
525 (yield timerPromise).callback();
527 let writer = yield writeStarted.promise;
528 // the first write has started
530 // dirty the data and request another save
531 let secondSave = tester.save(secondData);
533 // complete the first write
534 writer.resolve(firstData.length);
535 yield firstSave;
536 do_check_eq(tester.writtenData, firstData);
538 tester.save(thirdData);
539 let flushing = tester.flush();
541 yield secondSave;
542 do_check_eq(tester.writtenData, thirdData);
544 yield flushing;
545 do_check_eq(tester.writtenData, thirdData);
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 });