michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ michael@0: */ michael@0: michael@0: // Test behaviour of module to perform deferred save of data michael@0: // files to disk michael@0: michael@0: "use strict"; michael@0: michael@0: const testFile = gProfD.clone(); michael@0: testFile.append("DeferredSaveTest"); michael@0: michael@0: Components.utils.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: let DSContext = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {}); michael@0: let DeferredSave = DSContext.DeferredSave; michael@0: michael@0: // Test wrapper to let us do promise/task based testing of DeferredSave michael@0: function DeferredSaveTester(aDataProvider) { michael@0: let tester = { michael@0: // Deferred for the promise returned by the mock writeAtomic michael@0: waDeferred: null, michael@0: michael@0: // The most recent data "written" by the mock OS.File.writeAtomic michael@0: writtenData: undefined, michael@0: michael@0: dataToSave: "Data to save", michael@0: michael@0: save: (aData, aWriteHandler) => { michael@0: tester.writeHandler = aWriteHandler || writer; michael@0: tester.dataToSave = aData; michael@0: return tester.saver.saveChanges(); michael@0: }, michael@0: michael@0: flush: (aWriteHandler) => { michael@0: tester.writeHandler = aWriteHandler || writer; michael@0: return tester.saver.flush(); michael@0: }, michael@0: michael@0: get lastError() { michael@0: return tester.saver.lastError; michael@0: } michael@0: }; michael@0: michael@0: // Default write handler for most cases where the test case doesn't need michael@0: // to do anything while the write is in progress; just completes the write michael@0: // on the next event loop michael@0: function writer(aTester) { michael@0: do_print("default write callback"); michael@0: let length = aTester.writtenData.length; michael@0: do_execute_soon(() => aTester.waDeferred.resolve(length)); michael@0: } michael@0: michael@0: if (!aDataProvider) michael@0: aDataProvider = () => tester.dataToSave; michael@0: michael@0: tester.saver = new DeferredSave(testFile.path, aDataProvider); michael@0: michael@0: // Install a mock for OS.File.writeAtomic to let us control the async michael@0: // behaviour of the promise michael@0: DSContext.OS.File.writeAtomic = function mock_writeAtomic(aFile, aData, aOptions) { michael@0: do_print("writeAtomic: " + aFile + " data: '" + aData + "', " + aOptions.toSource()); michael@0: tester.writtenData = aData; michael@0: tester.waDeferred = Promise.defer(); michael@0: tester.writeHandler(tester); michael@0: return tester.waDeferred.promise; michael@0: }; michael@0: michael@0: return tester; michael@0: }; michael@0: michael@0: /** michael@0: * Install a mock nsITimer factory that triggers on the next spin of michael@0: * the event loop after it is scheduled michael@0: */ michael@0: function setQuickMockTimer() { michael@0: let quickTimer = { michael@0: initWithCallback: function(aFunction, aDelay, aType) { michael@0: do_print("Starting quick timer, delay = " + aDelay); michael@0: do_execute_soon(aFunction); michael@0: }, michael@0: cancel: function() { michael@0: do_throw("Attempted to cancel a quickMockTimer"); michael@0: } michael@0: }; michael@0: DSContext.MakeTimer = () => { michael@0: do_print("Creating quick timer"); michael@0: return quickTimer; michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Install a mock nsITimer factory in DeferredSave.jsm, returning a promise that resolves michael@0: * when the client code sets the timer. Test cases can use this to wait for client code to michael@0: * be ready for a timer event, and then signal the event by calling mockTimer.callback(). michael@0: * This could use some enhancement; clients can re-use the returned timer, michael@0: * but with this implementation it's not possible for the test to wait for michael@0: * a second call to initWithCallback() on the re-used timer. michael@0: * @return Promise{mockTimer} that resolves when initWithCallback() michael@0: * is called michael@0: */ michael@0: function setPromiseMockTimer() { michael@0: let waiter = Promise.defer(); michael@0: let mockTimer = { michael@0: callback: null, michael@0: delay: null, michael@0: type: null, michael@0: isCancelled: false, michael@0: michael@0: initWithCallback: function(aFunction, aDelay, aType) { michael@0: do_print("Starting timer, delay = " + aDelay); michael@0: this.callback = aFunction; michael@0: this.delay = aDelay; michael@0: this.type = aType; michael@0: // cancelled timers can be re-used michael@0: this.isCancelled = false; michael@0: waiter.resolve(this); michael@0: }, michael@0: cancel: function() { michael@0: do_print("Cancelled mock timer"); michael@0: this.callback = null; michael@0: this.delay = null; michael@0: this.type = null; michael@0: this.isCancelled = true; michael@0: // If initWithCallback was never called, resolve to let tests check for cancel michael@0: waiter.resolve(this); michael@0: } michael@0: }; michael@0: DSContext.MakeTimer = () => { michael@0: do_print("Creating mock timer"); michael@0: return mockTimer; michael@0: }; michael@0: return waiter.promise; michael@0: } michael@0: michael@0: /** michael@0: * Return a Promise that resolves after the specified number of milliseconds michael@0: */ michael@0: function delay(aDelayMS) { michael@0: let deferred = Promise.defer(); michael@0: do_timeout(aDelayMS, () => deferred.resolve(null)); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function run_test() { michael@0: run_next_test(); michael@0: } michael@0: michael@0: // Modify set data once, ask for save, make sure it saves cleanly michael@0: add_task(function test_basic_save_succeeds() { michael@0: setQuickMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let data = "Test 1 Data"; michael@0: michael@0: yield tester.save(data); michael@0: do_check_eq(tester.writtenData, data); michael@0: do_check_eq(1, tester.saver.totalSaves); michael@0: }); michael@0: michael@0: // Two saves called during the same event loop, both with callbacks michael@0: // Make sure we save only the second version of the data michael@0: add_task(function test_two_saves() { michael@0: setQuickMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let firstCallback_happened = false; michael@0: let firstData = "Test first save"; michael@0: let secondData = "Test second save"; michael@0: michael@0: // first save should not resolve until after the second one is called, michael@0: // so we can't just yield this promise michael@0: tester.save(firstData).then(count => { michael@0: do_check_eq(secondData, tester.writtenData); michael@0: do_check_false(firstCallback_happened); michael@0: firstCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: yield tester.save(secondData); michael@0: do_check_true(firstCallback_happened); michael@0: do_check_eq(secondData, tester.writtenData); michael@0: do_check_eq(1, tester.saver.totalSaves); michael@0: }); michael@0: michael@0: // Two saves called with a delay in between, both with callbacks michael@0: // Make sure we save the second version of the data michael@0: add_task(function test_two_saves_delay() { michael@0: let timerPromise = setPromiseMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let firstCallback_happened = false; michael@0: let delayDone = false; michael@0: michael@0: let firstData = "First data to save with delay"; michael@0: let secondData = "Modified data to save with delay"; michael@0: michael@0: tester.save(firstData).then(count => { michael@0: do_check_false(firstCallback_happened); michael@0: do_check_true(delayDone); michael@0: do_check_eq(secondData, tester.writtenData); michael@0: firstCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: // Wait a short time to let async events possibly spawned by the michael@0: // first tester.save() to run michael@0: yield delay(2); michael@0: delayDone = true; michael@0: // request to save modified data michael@0: let saving = tester.save(secondData); michael@0: // Yield to wait for client code to set the timer michael@0: let activeTimer = yield timerPromise; michael@0: // and then trigger it michael@0: activeTimer.callback(); michael@0: // now wait for the DeferredSave to finish saving michael@0: yield saving; michael@0: do_check_true(firstCallback_happened); michael@0: do_check_eq(secondData, tester.writtenData); michael@0: do_check_eq(1, tester.saver.totalSaves); michael@0: do_check_eq(0, tester.saver.overlappedSaves); michael@0: }); michael@0: michael@0: // Test case where OS.File immediately reports an error when the write begins michael@0: // Also check that the "error" getter correctly returns the error michael@0: // Then do a write that succeeds, and make sure the error is cleared michael@0: add_task(function test_error_immediate() { michael@0: let tester = DeferredSaveTester(); michael@0: let testError = new Error("Forced failure"); michael@0: function writeFail(aTester) { michael@0: aTester.waDeferred.reject(testError); michael@0: } michael@0: michael@0: setQuickMockTimer(); michael@0: yield tester.save("test_error_immediate", writeFail).then( michael@0: count => do_throw("Did not get expected error"), michael@0: error => do_check_eq(testError.message, error.message) michael@0: ); michael@0: do_check_eq(testError, tester.lastError); michael@0: michael@0: // This write should succeed and clear the error michael@0: yield tester.save("test_error_immediate succeeds"); michael@0: do_check_eq(null, tester.lastError); michael@0: // The failed save attempt counts in our total michael@0: do_check_eq(2, tester.saver.totalSaves); michael@0: }); michael@0: michael@0: // Save one set of changes, then while the write is in progress, modify the michael@0: // data two more times. Test that we re-write the dirty data exactly once michael@0: // after the first write succeeds michael@0: add_task(function dirty_while_writing() { michael@0: let tester = DeferredSaveTester(); michael@0: let firstData = "First data"; michael@0: let secondData = "Second data"; michael@0: let thirdData = "Third data"; michael@0: let firstCallback_happened = false; michael@0: let secondCallback_happened = false; michael@0: let writeStarted = Promise.defer(); michael@0: michael@0: function writeCallback(aTester) { michael@0: writeStarted.resolve(aTester.waDeferred); michael@0: } michael@0: michael@0: setQuickMockTimer(); michael@0: do_print("First save"); michael@0: tester.save(firstData, writeCallback).then( michael@0: count => { michael@0: do_check_false(firstCallback_happened); michael@0: do_check_false(secondCallback_happened); michael@0: do_check_eq(tester.writtenData, firstData); michael@0: firstCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: do_print("waiting for writer"); michael@0: let writer = yield writeStarted.promise; michael@0: do_print("Write started"); michael@0: michael@0: // Delay a bit, modify the data and call saveChanges, delay a bit more, michael@0: // modify the data and call saveChanges again, another delay, michael@0: // then complete the in-progress write michael@0: yield delay(1); michael@0: michael@0: tester.save(secondData).then( michael@0: count => { michael@0: do_check_true(firstCallback_happened); michael@0: do_check_false(secondCallback_happened); michael@0: do_check_eq(tester.writtenData, thirdData); michael@0: secondCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: // wait and then do the third change michael@0: yield delay(1); michael@0: let thirdWrite = tester.save(thirdData); michael@0: michael@0: // wait a bit more and then finally finish the first write michael@0: yield delay(1); michael@0: writer.resolve(firstData.length); michael@0: michael@0: // Now let everything else finish michael@0: yield thirdWrite; michael@0: do_check_true(firstCallback_happened); michael@0: do_check_true(secondCallback_happened); michael@0: do_check_eq(tester.writtenData, thirdData); michael@0: do_check_eq(2, tester.saver.totalSaves); michael@0: do_check_eq(1, tester.saver.overlappedSaves); michael@0: }); michael@0: michael@0: // A write callback for the OS.File.writeAtomic mock that rejects write attempts michael@0: function disabled_write_callback(aTester) { michael@0: do_throw("Should not have written during clean flush"); michael@0: deferred.reject(new Error("Write during supposedly clean flush")); michael@0: } michael@0: michael@0: // special write callback that disables itself to make sure michael@0: // we don't try to write twice michael@0: function write_then_disable(aTester) { michael@0: do_print("write_then_disable"); michael@0: let length = aTester.writtenData.length; michael@0: aTester.writeHandler = disabled_write_callback; michael@0: do_execute_soon(() => aTester.waDeferred.resolve(length)); michael@0: } michael@0: michael@0: // Flush tests. First, do an ordinary clean save and then call flush; michael@0: // there should not be another save michael@0: add_task(function flush_after_save() { michael@0: setQuickMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let dataToSave = "Flush after save"; michael@0: michael@0: yield tester.save(dataToSave); michael@0: yield tester.flush(disabled_write_callback); michael@0: do_check_eq(1, tester.saver.totalSaves); michael@0: }); michael@0: michael@0: // Flush while a write is in progress, but the in-memory data is clean michael@0: add_task(function flush_during_write() { michael@0: let tester = DeferredSaveTester(); michael@0: let dataToSave = "Flush during write"; michael@0: let firstCallback_happened = false; michael@0: let writeStarted = Promise.defer(); michael@0: michael@0: function writeCallback(aTester) { michael@0: writeStarted.resolve(aTester.waDeferred); michael@0: } michael@0: michael@0: setQuickMockTimer(); michael@0: tester.save(dataToSave, writeCallback).then( michael@0: count => { michael@0: do_check_false(firstCallback_happened); michael@0: firstCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: let writer = yield writeStarted.promise; michael@0: michael@0: // call flush with the write callback disabled, delay a bit more, complete in-progress write michael@0: let flushing = tester.flush(disabled_write_callback); michael@0: yield delay(2); michael@0: writer.resolve(dataToSave.length); michael@0: michael@0: // now wait for the flush to finish michael@0: yield flushing; michael@0: do_check_true(firstCallback_happened); michael@0: do_check_eq(1, tester.saver.totalSaves); michael@0: }); michael@0: michael@0: // Flush while dirty but write not in progress michael@0: // The data written should be the value at the time michael@0: // flush() is called, even if it is changed later michael@0: add_task(function flush_while_dirty() { michael@0: let timerPromise = setPromiseMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let firstData = "Flush while dirty, valid data"; michael@0: let firstCallback_happened = false; michael@0: michael@0: tester.save(firstData, write_then_disable).then( michael@0: count => { michael@0: do_check_false(firstCallback_happened); michael@0: firstCallback_happened = true; michael@0: do_check_eq(tester.writtenData, firstData); michael@0: }, do_report_unexpected_exception); michael@0: michael@0: // Wait for the timer to be set, but don't trigger it so the write won't start michael@0: let activeTimer = yield timerPromise; michael@0: michael@0: let flushing = tester.flush(); michael@0: michael@0: // Make sure the timer was cancelled michael@0: do_check_true(activeTimer.isCancelled); michael@0: michael@0: // Also make sure that data changed after the flush call michael@0: // (even without a saveChanges() call) doesn't get written michael@0: tester.dataToSave = "Flush while dirty, invalid data"; michael@0: michael@0: yield flushing; michael@0: do_check_true(firstCallback_happened); michael@0: do_check_eq(tester.writtenData, firstData); michael@0: do_check_eq(1, tester.saver.totalSaves); michael@0: }); michael@0: michael@0: // And the grand finale - modify the data, start writing, michael@0: // modify the data again so we're in progress and dirty, michael@0: // then flush, then modify the data again michael@0: // Data for the second write should be taken at the time michael@0: // flush() is called, even if it is modified later michael@0: add_task(function flush_writing_dirty() { michael@0: let timerPromise = setPromiseMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let firstData = "Flush first pass data"; michael@0: let secondData = "Flush second pass data"; michael@0: let firstCallback_happened = false; michael@0: let secondCallback_happened = false; michael@0: let writeStarted = Promise.defer(); michael@0: michael@0: function writeCallback(aTester) { michael@0: writeStarted.resolve(aTester.waDeferred); michael@0: } michael@0: michael@0: tester.save(firstData, writeCallback).then( michael@0: count => { michael@0: do_check_false(firstCallback_happened); michael@0: do_check_eq(tester.writtenData, firstData); michael@0: firstCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: // Trigger the timer callback as soon as the DeferredSave sets it michael@0: let activeTimer = yield timerPromise; michael@0: activeTimer.callback(); michael@0: let writer = yield writeStarted.promise; michael@0: // the first write has started michael@0: michael@0: // dirty the data and request another save michael@0: // after the second save completes, there should not be another write michael@0: tester.save(secondData, write_then_disable).then( michael@0: count => { michael@0: do_check_true(firstCallback_happened); michael@0: do_check_false(secondCallback_happened); michael@0: do_check_eq(tester.writtenData, secondData); michael@0: secondCallback_happened = true; michael@0: }, do_report_unexpected_exception); michael@0: michael@0: let flushing = tester.flush(write_then_disable); michael@0: // Flush should have cancelled our timer michael@0: do_check_true(activeTimer.isCancelled); michael@0: tester.dataToSave = "Flush, invalid data: changed late"; michael@0: // complete the first write michael@0: writer.resolve(firstData.length); michael@0: // now wait for the second write / flush to complete michael@0: yield flushing; michael@0: do_check_true(firstCallback_happened); michael@0: do_check_true(secondCallback_happened); michael@0: do_check_eq(tester.writtenData, secondData); michael@0: do_check_eq(2, tester.saver.totalSaves); michael@0: do_check_eq(1, tester.saver.overlappedSaves); michael@0: }); michael@0: michael@0: // A data provider callback that throws an error the first michael@0: // time it is called, and a different error the second time michael@0: // so that tests can (a) make sure the promise is rejected michael@0: // with the error and (b) make sure the provider is only michael@0: // called once in case of error michael@0: const expectedDataError = "Failed to serialize data"; michael@0: let badDataError = null; michael@0: function badDataProvider() { michael@0: let err = new Error(badDataError); michael@0: badDataError = "badDataProvider called twice"; michael@0: throw err; michael@0: } michael@0: michael@0: // Handle cases where data provider throws michael@0: // First, throws during a normal save michael@0: add_task(function data_throw() { michael@0: setQuickMockTimer(); michael@0: badDataError = expectedDataError; michael@0: let tester = DeferredSaveTester(badDataProvider); michael@0: yield tester.save("data_throw").then( michael@0: count => do_throw("Expected serialization failure"), michael@0: error => do_check_eq(error.message, expectedDataError)); michael@0: }); michael@0: michael@0: // Now, throws during flush michael@0: add_task(function data_throw_during_flush() { michael@0: badDataError = expectedDataError; michael@0: let tester = DeferredSaveTester(badDataProvider); michael@0: let firstCallback_happened = false; michael@0: michael@0: setPromiseMockTimer(); michael@0: // Write callback should never be called michael@0: tester.save("data_throw_during_flush", disabled_write_callback).then( michael@0: count => do_throw("Expected serialization failure"), michael@0: error => { michael@0: do_check_false(firstCallback_happened); michael@0: do_check_eq(error.message, expectedDataError); michael@0: firstCallback_happened = true; michael@0: }); michael@0: michael@0: // flush() will cancel the timer michael@0: yield tester.flush(disabled_write_callback).then( michael@0: count => do_throw("Expected serialization failure"), michael@0: error => do_check_eq(error.message, expectedDataError) michael@0: ); michael@0: michael@0: do_check_true(firstCallback_happened); michael@0: }); michael@0: michael@0: // Try to reproduce race condition. The observed sequence of events: michael@0: // saveChanges michael@0: // start writing michael@0: // saveChanges michael@0: // finish writing (need to restart delayed timer) michael@0: // saveChanges michael@0: // flush michael@0: // write starts michael@0: // actually restart timer for delayed write michael@0: // write completes michael@0: // delayed timer goes off, throws error because DeferredSave has been torn down michael@0: add_task(function delay_flush_race() { michael@0: let timerPromise = setPromiseMockTimer(); michael@0: let tester = DeferredSaveTester(); michael@0: let firstData = "First save"; michael@0: let secondData = "Second save"; michael@0: let thirdData = "Third save"; michael@0: let writeStarted = Promise.defer(); michael@0: michael@0: function writeCallback(aTester) { michael@0: writeStarted.resolve(aTester.waDeferred); michael@0: } michael@0: michael@0: // This promise won't resolve until after writeStarted michael@0: let firstSave = tester.save(firstData, writeCallback); michael@0: (yield timerPromise).callback(); michael@0: michael@0: let writer = yield writeStarted.promise; michael@0: // the first write has started michael@0: michael@0: // dirty the data and request another save michael@0: let secondSave = tester.save(secondData); michael@0: michael@0: // complete the first write michael@0: writer.resolve(firstData.length); michael@0: yield firstSave; michael@0: do_check_eq(tester.writtenData, firstData); michael@0: michael@0: tester.save(thirdData); michael@0: let flushing = tester.flush(); michael@0: michael@0: yield secondSave; michael@0: do_check_eq(tester.writtenData, thirdData); michael@0: michael@0: yield flushing; michael@0: do_check_eq(tester.writtenData, thirdData); michael@0: michael@0: // Our DeferredSave should not have a _timer here; if it michael@0: // does, the bug caused a reschedule michael@0: do_check_eq(null, tester.saver._timer); michael@0: });