1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,550 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ 1.6 + */ 1.7 + 1.8 +// Test behaviour of module to perform deferred save of data 1.9 +// files to disk 1.10 + 1.11 +"use strict"; 1.12 + 1.13 +const testFile = gProfD.clone(); 1.14 +testFile.append("DeferredSaveTest"); 1.15 + 1.16 +Components.utils.import("resource://gre/modules/Promise.jsm"); 1.17 + 1.18 +let DSContext = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {}); 1.19 +let DeferredSave = DSContext.DeferredSave; 1.20 + 1.21 +// Test wrapper to let us do promise/task based testing of DeferredSave 1.22 +function DeferredSaveTester(aDataProvider) { 1.23 + let tester = { 1.24 + // Deferred for the promise returned by the mock writeAtomic 1.25 + waDeferred: null, 1.26 + 1.27 + // The most recent data "written" by the mock OS.File.writeAtomic 1.28 + writtenData: undefined, 1.29 + 1.30 + dataToSave: "Data to save", 1.31 + 1.32 + save: (aData, aWriteHandler) => { 1.33 + tester.writeHandler = aWriteHandler || writer; 1.34 + tester.dataToSave = aData; 1.35 + return tester.saver.saveChanges(); 1.36 + }, 1.37 + 1.38 + flush: (aWriteHandler) => { 1.39 + tester.writeHandler = aWriteHandler || writer; 1.40 + return tester.saver.flush(); 1.41 + }, 1.42 + 1.43 + get lastError() { 1.44 + return tester.saver.lastError; 1.45 + } 1.46 + }; 1.47 + 1.48 + // Default write handler for most cases where the test case doesn't need 1.49 + // to do anything while the write is in progress; just completes the write 1.50 + // on the next event loop 1.51 + function writer(aTester) { 1.52 + do_print("default write callback"); 1.53 + let length = aTester.writtenData.length; 1.54 + do_execute_soon(() => aTester.waDeferred.resolve(length)); 1.55 + } 1.56 + 1.57 + if (!aDataProvider) 1.58 + aDataProvider = () => tester.dataToSave; 1.59 + 1.60 + tester.saver = new DeferredSave(testFile.path, aDataProvider); 1.61 + 1.62 + // Install a mock for OS.File.writeAtomic to let us control the async 1.63 + // behaviour of the promise 1.64 + DSContext.OS.File.writeAtomic = function mock_writeAtomic(aFile, aData, aOptions) { 1.65 + do_print("writeAtomic: " + aFile + " data: '" + aData + "', " + aOptions.toSource()); 1.66 + tester.writtenData = aData; 1.67 + tester.waDeferred = Promise.defer(); 1.68 + tester.writeHandler(tester); 1.69 + return tester.waDeferred.promise; 1.70 + }; 1.71 + 1.72 + return tester; 1.73 +}; 1.74 + 1.75 +/** 1.76 + * Install a mock nsITimer factory that triggers on the next spin of 1.77 + * the event loop after it is scheduled 1.78 + */ 1.79 +function setQuickMockTimer() { 1.80 + let quickTimer = { 1.81 + initWithCallback: function(aFunction, aDelay, aType) { 1.82 + do_print("Starting quick timer, delay = " + aDelay); 1.83 + do_execute_soon(aFunction); 1.84 + }, 1.85 + cancel: function() { 1.86 + do_throw("Attempted to cancel a quickMockTimer"); 1.87 + } 1.88 + }; 1.89 + DSContext.MakeTimer = () => { 1.90 + do_print("Creating quick timer"); 1.91 + return quickTimer; 1.92 + }; 1.93 +} 1.94 + 1.95 +/** 1.96 + * Install a mock nsITimer factory in DeferredSave.jsm, returning a promise that resolves 1.97 + * when the client code sets the timer. Test cases can use this to wait for client code to 1.98 + * be ready for a timer event, and then signal the event by calling mockTimer.callback(). 1.99 + * This could use some enhancement; clients can re-use the returned timer, 1.100 + * but with this implementation it's not possible for the test to wait for 1.101 + * a second call to initWithCallback() on the re-used timer. 1.102 + * @return Promise{mockTimer} that resolves when initWithCallback() 1.103 + * is called 1.104 + */ 1.105 +function setPromiseMockTimer() { 1.106 + let waiter = Promise.defer(); 1.107 + let mockTimer = { 1.108 + callback: null, 1.109 + delay: null, 1.110 + type: null, 1.111 + isCancelled: false, 1.112 + 1.113 + initWithCallback: function(aFunction, aDelay, aType) { 1.114 + do_print("Starting timer, delay = " + aDelay); 1.115 + this.callback = aFunction; 1.116 + this.delay = aDelay; 1.117 + this.type = aType; 1.118 + // cancelled timers can be re-used 1.119 + this.isCancelled = false; 1.120 + waiter.resolve(this); 1.121 + }, 1.122 + cancel: function() { 1.123 + do_print("Cancelled mock timer"); 1.124 + this.callback = null; 1.125 + this.delay = null; 1.126 + this.type = null; 1.127 + this.isCancelled = true; 1.128 + // If initWithCallback was never called, resolve to let tests check for cancel 1.129 + waiter.resolve(this); 1.130 + } 1.131 + }; 1.132 + DSContext.MakeTimer = () => { 1.133 + do_print("Creating mock timer"); 1.134 + return mockTimer; 1.135 + }; 1.136 + return waiter.promise; 1.137 +} 1.138 + 1.139 +/** 1.140 + * Return a Promise<null> that resolves after the specified number of milliseconds 1.141 + */ 1.142 +function delay(aDelayMS) { 1.143 + let deferred = Promise.defer(); 1.144 + do_timeout(aDelayMS, () => deferred.resolve(null)); 1.145 + return deferred.promise; 1.146 +} 1.147 + 1.148 +function run_test() { 1.149 + run_next_test(); 1.150 +} 1.151 + 1.152 +// Modify set data once, ask for save, make sure it saves cleanly 1.153 +add_task(function test_basic_save_succeeds() { 1.154 + setQuickMockTimer(); 1.155 + let tester = DeferredSaveTester(); 1.156 + let data = "Test 1 Data"; 1.157 + 1.158 + yield tester.save(data); 1.159 + do_check_eq(tester.writtenData, data); 1.160 + do_check_eq(1, tester.saver.totalSaves); 1.161 +}); 1.162 + 1.163 +// Two saves called during the same event loop, both with callbacks 1.164 +// Make sure we save only the second version of the data 1.165 +add_task(function test_two_saves() { 1.166 + setQuickMockTimer(); 1.167 + let tester = DeferredSaveTester(); 1.168 + let firstCallback_happened = false; 1.169 + let firstData = "Test first save"; 1.170 + let secondData = "Test second save"; 1.171 + 1.172 + // first save should not resolve until after the second one is called, 1.173 + // so we can't just yield this promise 1.174 + tester.save(firstData).then(count => { 1.175 + do_check_eq(secondData, tester.writtenData); 1.176 + do_check_false(firstCallback_happened); 1.177 + firstCallback_happened = true; 1.178 + }, do_report_unexpected_exception); 1.179 + 1.180 + yield tester.save(secondData); 1.181 + do_check_true(firstCallback_happened); 1.182 + do_check_eq(secondData, tester.writtenData); 1.183 + do_check_eq(1, tester.saver.totalSaves); 1.184 +}); 1.185 + 1.186 +// Two saves called with a delay in between, both with callbacks 1.187 +// Make sure we save the second version of the data 1.188 +add_task(function test_two_saves_delay() { 1.189 + let timerPromise = setPromiseMockTimer(); 1.190 + let tester = DeferredSaveTester(); 1.191 + let firstCallback_happened = false; 1.192 + let delayDone = false; 1.193 + 1.194 + let firstData = "First data to save with delay"; 1.195 + let secondData = "Modified data to save with delay"; 1.196 + 1.197 + tester.save(firstData).then(count => { 1.198 + do_check_false(firstCallback_happened); 1.199 + do_check_true(delayDone); 1.200 + do_check_eq(secondData, tester.writtenData); 1.201 + firstCallback_happened = true; 1.202 + }, do_report_unexpected_exception); 1.203 + 1.204 + // Wait a short time to let async events possibly spawned by the 1.205 + // first tester.save() to run 1.206 + yield delay(2); 1.207 + delayDone = true; 1.208 + // request to save modified data 1.209 + let saving = tester.save(secondData); 1.210 + // Yield to wait for client code to set the timer 1.211 + let activeTimer = yield timerPromise; 1.212 + // and then trigger it 1.213 + activeTimer.callback(); 1.214 + // now wait for the DeferredSave to finish saving 1.215 + yield saving; 1.216 + do_check_true(firstCallback_happened); 1.217 + do_check_eq(secondData, tester.writtenData); 1.218 + do_check_eq(1, tester.saver.totalSaves); 1.219 + do_check_eq(0, tester.saver.overlappedSaves); 1.220 +}); 1.221 + 1.222 +// Test case where OS.File immediately reports an error when the write begins 1.223 +// Also check that the "error" getter correctly returns the error 1.224 +// Then do a write that succeeds, and make sure the error is cleared 1.225 +add_task(function test_error_immediate() { 1.226 + let tester = DeferredSaveTester(); 1.227 + let testError = new Error("Forced failure"); 1.228 + function writeFail(aTester) { 1.229 + aTester.waDeferred.reject(testError); 1.230 + } 1.231 + 1.232 + setQuickMockTimer(); 1.233 + yield tester.save("test_error_immediate", writeFail).then( 1.234 + count => do_throw("Did not get expected error"), 1.235 + error => do_check_eq(testError.message, error.message) 1.236 + ); 1.237 + do_check_eq(testError, tester.lastError); 1.238 + 1.239 + // This write should succeed and clear the error 1.240 + yield tester.save("test_error_immediate succeeds"); 1.241 + do_check_eq(null, tester.lastError); 1.242 + // The failed save attempt counts in our total 1.243 + do_check_eq(2, tester.saver.totalSaves); 1.244 +}); 1.245 + 1.246 +// Save one set of changes, then while the write is in progress, modify the 1.247 +// data two more times. Test that we re-write the dirty data exactly once 1.248 +// after the first write succeeds 1.249 +add_task(function dirty_while_writing() { 1.250 + let tester = DeferredSaveTester(); 1.251 + let firstData = "First data"; 1.252 + let secondData = "Second data"; 1.253 + let thirdData = "Third data"; 1.254 + let firstCallback_happened = false; 1.255 + let secondCallback_happened = false; 1.256 + let writeStarted = Promise.defer(); 1.257 + 1.258 + function writeCallback(aTester) { 1.259 + writeStarted.resolve(aTester.waDeferred); 1.260 + } 1.261 + 1.262 + setQuickMockTimer(); 1.263 + do_print("First save"); 1.264 + tester.save(firstData, writeCallback).then( 1.265 + count => { 1.266 + do_check_false(firstCallback_happened); 1.267 + do_check_false(secondCallback_happened); 1.268 + do_check_eq(tester.writtenData, firstData); 1.269 + firstCallback_happened = true; 1.270 + }, do_report_unexpected_exception); 1.271 + 1.272 + do_print("waiting for writer"); 1.273 + let writer = yield writeStarted.promise; 1.274 + do_print("Write started"); 1.275 + 1.276 + // Delay a bit, modify the data and call saveChanges, delay a bit more, 1.277 + // modify the data and call saveChanges again, another delay, 1.278 + // then complete the in-progress write 1.279 + yield delay(1); 1.280 + 1.281 + tester.save(secondData).then( 1.282 + count => { 1.283 + do_check_true(firstCallback_happened); 1.284 + do_check_false(secondCallback_happened); 1.285 + do_check_eq(tester.writtenData, thirdData); 1.286 + secondCallback_happened = true; 1.287 + }, do_report_unexpected_exception); 1.288 + 1.289 + // wait and then do the third change 1.290 + yield delay(1); 1.291 + let thirdWrite = tester.save(thirdData); 1.292 + 1.293 + // wait a bit more and then finally finish the first write 1.294 + yield delay(1); 1.295 + writer.resolve(firstData.length); 1.296 + 1.297 + // Now let everything else finish 1.298 + yield thirdWrite; 1.299 + do_check_true(firstCallback_happened); 1.300 + do_check_true(secondCallback_happened); 1.301 + do_check_eq(tester.writtenData, thirdData); 1.302 + do_check_eq(2, tester.saver.totalSaves); 1.303 + do_check_eq(1, tester.saver.overlappedSaves); 1.304 +}); 1.305 + 1.306 +// A write callback for the OS.File.writeAtomic mock that rejects write attempts 1.307 +function disabled_write_callback(aTester) { 1.308 + do_throw("Should not have written during clean flush"); 1.309 + deferred.reject(new Error("Write during supposedly clean flush")); 1.310 +} 1.311 + 1.312 +// special write callback that disables itself to make sure 1.313 +// we don't try to write twice 1.314 +function write_then_disable(aTester) { 1.315 + do_print("write_then_disable"); 1.316 + let length = aTester.writtenData.length; 1.317 + aTester.writeHandler = disabled_write_callback; 1.318 + do_execute_soon(() => aTester.waDeferred.resolve(length)); 1.319 +} 1.320 + 1.321 +// Flush tests. First, do an ordinary clean save and then call flush; 1.322 +// there should not be another save 1.323 +add_task(function flush_after_save() { 1.324 + setQuickMockTimer(); 1.325 + let tester = DeferredSaveTester(); 1.326 + let dataToSave = "Flush after save"; 1.327 + 1.328 + yield tester.save(dataToSave); 1.329 + yield tester.flush(disabled_write_callback); 1.330 + do_check_eq(1, tester.saver.totalSaves); 1.331 +}); 1.332 + 1.333 +// Flush while a write is in progress, but the in-memory data is clean 1.334 +add_task(function flush_during_write() { 1.335 + let tester = DeferredSaveTester(); 1.336 + let dataToSave = "Flush during write"; 1.337 + let firstCallback_happened = false; 1.338 + let writeStarted = Promise.defer(); 1.339 + 1.340 + function writeCallback(aTester) { 1.341 + writeStarted.resolve(aTester.waDeferred); 1.342 + } 1.343 + 1.344 + setQuickMockTimer(); 1.345 + tester.save(dataToSave, writeCallback).then( 1.346 + count => { 1.347 + do_check_false(firstCallback_happened); 1.348 + firstCallback_happened = true; 1.349 + }, do_report_unexpected_exception); 1.350 + 1.351 + let writer = yield writeStarted.promise; 1.352 + 1.353 + // call flush with the write callback disabled, delay a bit more, complete in-progress write 1.354 + let flushing = tester.flush(disabled_write_callback); 1.355 + yield delay(2); 1.356 + writer.resolve(dataToSave.length); 1.357 + 1.358 + // now wait for the flush to finish 1.359 + yield flushing; 1.360 + do_check_true(firstCallback_happened); 1.361 + do_check_eq(1, tester.saver.totalSaves); 1.362 +}); 1.363 + 1.364 +// Flush while dirty but write not in progress 1.365 +// The data written should be the value at the time 1.366 +// flush() is called, even if it is changed later 1.367 +add_task(function flush_while_dirty() { 1.368 + let timerPromise = setPromiseMockTimer(); 1.369 + let tester = DeferredSaveTester(); 1.370 + let firstData = "Flush while dirty, valid data"; 1.371 + let firstCallback_happened = false; 1.372 + 1.373 + tester.save(firstData, write_then_disable).then( 1.374 + count => { 1.375 + do_check_false(firstCallback_happened); 1.376 + firstCallback_happened = true; 1.377 + do_check_eq(tester.writtenData, firstData); 1.378 + }, do_report_unexpected_exception); 1.379 + 1.380 + // Wait for the timer to be set, but don't trigger it so the write won't start 1.381 + let activeTimer = yield timerPromise; 1.382 + 1.383 + let flushing = tester.flush(); 1.384 + 1.385 + // Make sure the timer was cancelled 1.386 + do_check_true(activeTimer.isCancelled); 1.387 + 1.388 + // Also make sure that data changed after the flush call 1.389 + // (even without a saveChanges() call) doesn't get written 1.390 + tester.dataToSave = "Flush while dirty, invalid data"; 1.391 + 1.392 + yield flushing; 1.393 + do_check_true(firstCallback_happened); 1.394 + do_check_eq(tester.writtenData, firstData); 1.395 + do_check_eq(1, tester.saver.totalSaves); 1.396 +}); 1.397 + 1.398 +// And the grand finale - modify the data, start writing, 1.399 +// modify the data again so we're in progress and dirty, 1.400 +// then flush, then modify the data again 1.401 +// Data for the second write should be taken at the time 1.402 +// flush() is called, even if it is modified later 1.403 +add_task(function flush_writing_dirty() { 1.404 + let timerPromise = setPromiseMockTimer(); 1.405 + let tester = DeferredSaveTester(); 1.406 + let firstData = "Flush first pass data"; 1.407 + let secondData = "Flush second pass data"; 1.408 + let firstCallback_happened = false; 1.409 + let secondCallback_happened = false; 1.410 + let writeStarted = Promise.defer(); 1.411 + 1.412 + function writeCallback(aTester) { 1.413 + writeStarted.resolve(aTester.waDeferred); 1.414 + } 1.415 + 1.416 + tester.save(firstData, writeCallback).then( 1.417 + count => { 1.418 + do_check_false(firstCallback_happened); 1.419 + do_check_eq(tester.writtenData, firstData); 1.420 + firstCallback_happened = true; 1.421 + }, do_report_unexpected_exception); 1.422 + 1.423 + // Trigger the timer callback as soon as the DeferredSave sets it 1.424 + let activeTimer = yield timerPromise; 1.425 + activeTimer.callback(); 1.426 + let writer = yield writeStarted.promise; 1.427 + // the first write has started 1.428 + 1.429 + // dirty the data and request another save 1.430 + // after the second save completes, there should not be another write 1.431 + tester.save(secondData, write_then_disable).then( 1.432 + count => { 1.433 + do_check_true(firstCallback_happened); 1.434 + do_check_false(secondCallback_happened); 1.435 + do_check_eq(tester.writtenData, secondData); 1.436 + secondCallback_happened = true; 1.437 + }, do_report_unexpected_exception); 1.438 + 1.439 + let flushing = tester.flush(write_then_disable); 1.440 + // Flush should have cancelled our timer 1.441 + do_check_true(activeTimer.isCancelled); 1.442 + tester.dataToSave = "Flush, invalid data: changed late"; 1.443 + // complete the first write 1.444 + writer.resolve(firstData.length); 1.445 + // now wait for the second write / flush to complete 1.446 + yield flushing; 1.447 + do_check_true(firstCallback_happened); 1.448 + do_check_true(secondCallback_happened); 1.449 + do_check_eq(tester.writtenData, secondData); 1.450 + do_check_eq(2, tester.saver.totalSaves); 1.451 + do_check_eq(1, tester.saver.overlappedSaves); 1.452 +}); 1.453 + 1.454 +// A data provider callback that throws an error the first 1.455 +// time it is called, and a different error the second time 1.456 +// so that tests can (a) make sure the promise is rejected 1.457 +// with the error and (b) make sure the provider is only 1.458 +// called once in case of error 1.459 +const expectedDataError = "Failed to serialize data"; 1.460 +let badDataError = null; 1.461 +function badDataProvider() { 1.462 + let err = new Error(badDataError); 1.463 + badDataError = "badDataProvider called twice"; 1.464 + throw err; 1.465 +} 1.466 + 1.467 +// Handle cases where data provider throws 1.468 +// First, throws during a normal save 1.469 +add_task(function data_throw() { 1.470 + setQuickMockTimer(); 1.471 + badDataError = expectedDataError; 1.472 + let tester = DeferredSaveTester(badDataProvider); 1.473 + yield tester.save("data_throw").then( 1.474 + count => do_throw("Expected serialization failure"), 1.475 + error => do_check_eq(error.message, expectedDataError)); 1.476 +}); 1.477 + 1.478 +// Now, throws during flush 1.479 +add_task(function data_throw_during_flush() { 1.480 + badDataError = expectedDataError; 1.481 + let tester = DeferredSaveTester(badDataProvider); 1.482 + let firstCallback_happened = false; 1.483 + 1.484 + setPromiseMockTimer(); 1.485 + // Write callback should never be called 1.486 + tester.save("data_throw_during_flush", disabled_write_callback).then( 1.487 + count => do_throw("Expected serialization failure"), 1.488 + error => { 1.489 + do_check_false(firstCallback_happened); 1.490 + do_check_eq(error.message, expectedDataError); 1.491 + firstCallback_happened = true; 1.492 + }); 1.493 + 1.494 + // flush() will cancel the timer 1.495 + yield tester.flush(disabled_write_callback).then( 1.496 + count => do_throw("Expected serialization failure"), 1.497 + error => do_check_eq(error.message, expectedDataError) 1.498 + ); 1.499 + 1.500 + do_check_true(firstCallback_happened); 1.501 +}); 1.502 + 1.503 +// Try to reproduce race condition. The observed sequence of events: 1.504 +// saveChanges 1.505 +// start writing 1.506 +// saveChanges 1.507 +// finish writing (need to restart delayed timer) 1.508 +// saveChanges 1.509 +// flush 1.510 +// write starts 1.511 +// actually restart timer for delayed write 1.512 +// write completes 1.513 +// delayed timer goes off, throws error because DeferredSave has been torn down 1.514 +add_task(function delay_flush_race() { 1.515 + let timerPromise = setPromiseMockTimer(); 1.516 + let tester = DeferredSaveTester(); 1.517 + let firstData = "First save"; 1.518 + let secondData = "Second save"; 1.519 + let thirdData = "Third save"; 1.520 + let writeStarted = Promise.defer(); 1.521 + 1.522 + function writeCallback(aTester) { 1.523 + writeStarted.resolve(aTester.waDeferred); 1.524 + } 1.525 + 1.526 + // This promise won't resolve until after writeStarted 1.527 + let firstSave = tester.save(firstData, writeCallback); 1.528 + (yield timerPromise).callback(); 1.529 + 1.530 + let writer = yield writeStarted.promise; 1.531 + // the first write has started 1.532 + 1.533 + // dirty the data and request another save 1.534 + let secondSave = tester.save(secondData); 1.535 + 1.536 + // complete the first write 1.537 + writer.resolve(firstData.length); 1.538 + yield firstSave; 1.539 + do_check_eq(tester.writtenData, firstData); 1.540 + 1.541 + tester.save(thirdData); 1.542 + let flushing = tester.flush(); 1.543 + 1.544 + yield secondSave; 1.545 + do_check_eq(tester.writtenData, thirdData); 1.546 + 1.547 + yield flushing; 1.548 + do_check_eq(tester.writtenData, thirdData); 1.549 + 1.550 + // Our DeferredSave should not have a _timer here; if it 1.551 + // does, the bug caused a reschedule 1.552 + do_check_eq(null, tester.saver._timer); 1.553 +});