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