mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-09-13 16:55:23 +00:00
551 lines
18 KiB
JavaScript
551 lines
18 KiB
JavaScript
/* 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");
|
|
deferred.reject(new Error("Write during supposedly 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);
|
|
});
|