toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 });

mercurial