diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js new file mode 100644 index 00000000000..156468ba1a8 --- /dev/null +++ b/services/sync/modules/async.js @@ -0,0 +1,337 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Firefox Sync. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Richard Newman + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Async']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +// Constants for makeSyncCallback, waitForSyncCallback. +const CB_READY = {}; +const CB_COMPLETE = {}; +const CB_FAIL = {}; + +const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR; + +Cu.import("resource://services-sync/util.js"); + +/* + * Helpers for various async operations. + */ +let Async = { + + /** + * Helpers for making asynchronous calls within a synchronous API possible. + * + * If you value your sanity, do not look closely at the following functions. + */ + + /** + * Create a sync callback that remembers state, in particular whether it has + * been called. + */ + makeSyncCallback: function makeSyncCallback() { + // The main callback remembers the value it was passed, and that it got data. + let onComplete = function onComplete(data) { + onComplete.state = CB_COMPLETE; + onComplete.value = data; + }; + + // Initialize private callback data in preparation for being called. + onComplete.state = CB_READY; + onComplete.value = null; + + // Allow an alternate callback to trigger an exception to be thrown. + onComplete.throw = function onComplete_throw(data) { + onComplete.state = CB_FAIL; + onComplete.value = data; + + // Cause the caller to get an exception and stop execution. + throw data; + }; + + return onComplete; + }, + + /** + * Wait for a sync callback to finish. + */ + waitForSyncCallback: function waitForSyncCallback(callback) { + // Grab the current thread so we can make it give up priority. + let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + // Keep waiting until our callback is triggered (unless the app is quitting). + while (Utils.checkAppReady() && callback.state == CB_READY) { + thread.processNextEvent(true); + } + + // Reset the state of the callback to prepare for another call. + let state = callback.state; + callback.state = CB_READY; + + // Throw the value the callback decided to fail with. + if (state == CB_FAIL) { + throw callback.value; + } + + // Return the value passed to the callback. + return callback.value; + }, + + /** + * Return the two things you need to make an asynchronous call synchronous + * by spinning the event loop. + */ + makeSpinningCallback: function makeSpinningCallback() { + let cb = Async.makeSyncCallback(); + function callback(error, ret) { + if (error) + cb.throw(error); + cb(ret); + } + callback.wait = function() Async.waitForSyncCallback(cb); + return callback; + }, + + /** + * Synchronously invoke a method that takes only a `callback` argument. + */ + callSpinningly: function callSpinningly(self, method) { + let callback = this.makeSpinningCallback(); + method.call(self, callback); + return callback.wait(); + }, + + /* + * Produce a sequence of callbacks which -- when all have been executed + * successfully *or* any have failed -- invoke the output callback. + * + * Returns a generator. + * + * Each input callback should have the signature (error, result), and should + * return a truthy value if the computation should be considered to have + * failed. + * + * The contents of ".data" on each input callback are copied to the + * resultant callback items. This can save some effort on the caller's side. + * + * These callbacks are assumed to be single- or double-valued (a "result" and + * a "context", say), which covers the common cases without the expense of + * `arguments`. + */ + barrieredCallbacks: function (callbacks, output) { + if (!output) { + throw "No output callback provided to barrieredCallbacks."; + } + + let counter = callbacks.length; + function makeCb(input) { + let cb = function(error, result, context) { + if (!output) { + return; + } + + let err; + try { + err = input(error, result, context); + } catch (ex) { + output(ex); + output = undefined; + return; + } + if ((0 == --counter) || err) { + output(err); + output = undefined; + } + }; + cb.data = input.data; + return cb; + } + return (makeCb(i) for each (i in callbacks)); + }, + + /* + * Similar to barrieredCallbacks, but with the same callback each time. + */ + countedCallback: function (componentCb, count, output) { + if (!output) { + throw "No output callback provided to countedCallback."; + } + + if (!count || (count <= 0)) { + throw "Invalid count provided to countedCallback."; + } + + let counter = count; + return function (error, result, context) { + if (!output) { + return; + } + + let err; + try { + err = componentCb(error, result, context); + } catch (ex) { + output(ex); + // We're done; make sure output callback is only called once. + output = undefined; + return; + } + if ((0 == --counter) || err) { + output(err); // If this throws, then... oh well. + output = undefined; + return; + } + }; + }, + + /* + * Invoke `f` with each item and a wrapped version of `componentCb`. + * When each component callback is invoked, the next invocation of `f` is + * begun, unless the return value is truthy. (See barrieredCallbacks.) + * + * Finally, invoke the output callback. + * + * If there are no items, the output callback is invoked immediately. + */ + serially: function serially(items, f, componentCb, output) { + if (!output) { + throw "No output callback provided to serially."; + } + + if (!items || !items.length) { + output(); + return; + } + + let count = items.length; + let i = 0; + function cb(error, result, context) { + let err = error; + if (!err) { + try { + err = componentCb(error, result, context); + } catch (ex) { + err = ex; + } + } + if ((++i == count) || err) { + output(err); + return; + } + Utils.delay(function () { f(items[i], cb); }); + } + f(items[i], cb); + }, + + /* + * Return a callback which executes `f` then `callback`, regardless of + * whether it was invoked with an error. If an exception is thrown during the + * evaluation of `f`, it takes precedence over an error provided to the + * callback. + * + * When used to wrap a callback, this offers similar behavior to try..finally + * in plain JavaScript. + */ + finallyCallback: function (callback, f) { + return function(err) { + try { + f(); + callback(err); + } catch (ex) { + callback(ex); + } + }; + }, + + // Prototype for mozIStorageCallback, used in querySpinningly. + // This allows us to define the handle* functions just once rather + // than on every querySpinningly invocation. + _storageCallbackPrototype: { + results: null, + + // These are set by queryAsync. + names: null, + syncCb: null, + + handleResult: function handleResult(results) { + if (!this.names) { + return; + } + if (!this.results) { + this.results = []; + } + let row; + while ((row = results.getNextRow()) != null) { + let item = {}; + for each (name in this.names) { + item[name] = row.getResultByName(name); + } + this.results.push(item); + } + }, + handleError: function handleError(error) { + this.syncCb.throw(error); + }, + handleCompletion: function handleCompletion(reason) { + + // If we got an error, handleError will also have been called, so don't + // call the callback! We never cancel statements, so we don't need to + // address that quandary. + if (reason == REASON_ERROR) + return; + + // If we were called with column names but didn't find any results, + // the calling code probably still expects an array as a return value. + if (this.names && !this.results) { + this.results = []; + } + this.syncCb(this.results); + } + }, + + querySpinningly: function querySpinningly(query, names) { + // 'Synchronously' asyncExecute, fetching all results by name. + let storageCallback = {names: names, + syncCb: Async.makeSyncCallback()}; + storageCallback.__proto__ = Async._storageCallbackPrototype; + query.executeAsync(storageCallback); + return Async.waitForSyncCallback(storageCallback.syncCb); + }, +}; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5f68a8e005d..0037dcf8b8c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -46,6 +46,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/ext/Observers.js"); @@ -200,10 +201,10 @@ function Store(name) { Store.prototype = { _sleep: function _sleep(delay) { - let cb = Utils.makeSyncCallback(); + let cb = Async.makeSyncCallback(); this._timer.initWithCallback({notify: cb}, delay, Ci.nsITimer.TYPE_ONE_SHOT); - Utils.waitForSyncCallback(cb); + Async.waitForSyncCallback(cb); }, applyIncomingBatch: function applyIncomingBatch(records) { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d0f2f7e783c..e0f44a08142 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -68,6 +68,7 @@ Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/record.js"); +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/main.js"); // For access to Service. @@ -932,7 +933,7 @@ BookmarksStore.prototype = { _getChildGUIDsForId: function _getChildGUIDsForId(itemid) { let stmt = this._childGUIDsStm; stmt.params.parent = itemid; - let rows = Utils.queryAsync(stmt, this._childGUIDsCols); + let rows = Async.querySpinningly(stmt, this._childGUIDsCols); return rows.map(function (row) { if (row.guid) { return row.guid; @@ -1075,7 +1076,7 @@ BookmarksStore.prototype = { let stmt = this._setGUIDStm; stmt.params.guid = guid; stmt.params.item_id = id; - Utils.queryAsync(stmt); + Async.querySpinningly(stmt); return guid; }, @@ -1096,7 +1097,7 @@ BookmarksStore.prototype = { stmt.params.item_id = id; // Use the existing GUID if it exists - let result = Utils.queryAsync(stmt, this._guidForIdCols)[0]; + let result = Async.querySpinningly(stmt, this._guidForIdCols)[0]; if (result && result.guid) return result.guid; @@ -1122,7 +1123,7 @@ BookmarksStore.prototype = { // guid might be a String object rather than a string. stmt.params.guid = guid.toString(); - let results = Utils.queryAsync(stmt, this._idForGUIDCols); + let results = Async.querySpinningly(stmt, this._idForGUIDCols); this._log.trace("Number of rows matching GUID " + guid + ": " + results.length); @@ -1149,7 +1150,7 @@ BookmarksStore.prototype = { // Add in the bookmark's frecency if we have something if (record.bmkUri != null) { this._frecencyStm.params.url = record.bmkUri; - let result = Utils.queryAsync(this._frecencyStm, this._frecencyCols); + let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols); if (result.length) index += result[0].frecency; } diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index c8a5e9c8c30..d72d86618b1 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -43,6 +43,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/record.js"); +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/log4moz.js"); @@ -72,14 +73,14 @@ let FormWrapper = { "((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * " + "timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) DESC " + "LIMIT 500"); - return Utils.queryAsync(query, ["name", "value"]); + return Async.querySpinningly(query, ["name", "value"]); }, getEntry: function getEntry(guid) { let query = Svc.Form.DBConnection.createAsyncStatement( "SELECT fieldname name, value FROM moz_formhistory WHERE guid = :guid"); query.params.guid = guid; - return Utils.queryAsync(query, ["name", "value"])[0]; + return Async.querySpinningly(query, ["name", "value"])[0]; }, getGUID: function getGUID(name, value) { @@ -91,7 +92,7 @@ let FormWrapper = { getQuery.params.value = value; // Give the guid if we found one - let item = Utils.queryAsync(getQuery, ["guid"])[0]; + let item = Async.querySpinningly(getQuery, ["guid"])[0]; if (!item) { // Shouldn't happen, but Bug 597400... @@ -113,7 +114,7 @@ let FormWrapper = { setQuery.params.guid = guid; setQuery.params.name = name; setQuery.params.value = value; - Utils.queryAsync(setQuery); + Async.querySpinningly(setQuery); return guid; }, @@ -122,7 +123,7 @@ let FormWrapper = { let query = Svc.Form.DBConnection.createAsyncStatement( "SELECT guid FROM moz_formhistory WHERE guid = :guid LIMIT 1"); query.params.guid = guid; - return Utils.queryAsync(query, ["guid"]).length == 1; + return Async.querySpinningly(query, ["guid"]).length == 1; }, replaceGUID: function replaceGUID(oldGUID, newGUID) { @@ -130,7 +131,7 @@ let FormWrapper = { "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID"); query.params.oldGUID = oldGUID; query.params.newGUID = newGUID; - Utils.queryAsync(query); + Async.querySpinningly(query); } }; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 9641ccd7edb..fc133fc14de 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -51,6 +51,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/record.js"); +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/log4moz.js"); @@ -132,7 +133,7 @@ HistoryStore.prototype = { let stmt = this._setGUIDStm; stmt.params.guid = guid; stmt.params.page_url = uri; - Utils.queryAsync(stmt); + Async.querySpinningly(stmt); return guid; }, @@ -149,7 +150,7 @@ HistoryStore.prototype = { stm.params.page_url = uri.spec ? uri.spec : uri; // Use the existing GUID if it exists - let result = Utils.queryAsync(stm, this._guidCols)[0]; + let result = Async.querySpinningly(stm, this._guidCols)[0]; if (result && result.guid) return result.guid; @@ -188,13 +189,13 @@ HistoryStore.prototype = { // See bug 320831 for why we use SQL here _getVisits: function HistStore__getVisits(uri) { this._visitStm.params.url = uri; - return Utils.queryAsync(this._visitStm, this._visitCols); + return Async.querySpinningly(this._visitStm, this._visitCols); }, // See bug 468732 for why we use SQL here _findURLByGUID: function HistStore__findURLByGUID(guid) { this._urlStm.params.guid = guid; - return Utils.queryAsync(this._urlStm, this._urlCols)[0]; + return Async.querySpinningly(this._urlStm, this._urlCols)[0]; }, changeItemID: function HStore_changeItemID(oldID, newID) { @@ -207,7 +208,7 @@ HistoryStore.prototype = { this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000; this._allUrlStm.params.max_results = MAX_HISTORY_UPLOAD; - let urls = Utils.queryAsync(this._allUrlStm, this._allUrlCols); + let urls = Async.querySpinningly(this._allUrlStm, this._allUrlCols); let self = this; return urls.reduce(function(ids, item) { ids[self.GUIDForUri(item.url, true)] = item.url; @@ -251,7 +252,7 @@ HistoryStore.prototype = { return failed; } - let cb = Utils.makeSyncCallback(); + let cb = Async.makeSyncCallback(); let onPlace = function onPlace(result, placeInfo) { if (!Components.isSuccessCode(result)) { failed.push(placeInfo.guid); @@ -263,7 +264,7 @@ HistoryStore.prototype = { }; Svc.Obs.add(TOPIC_UPDATEPLACES_COMPLETE, onComplete); this._asyncHistory.updatePlaces(records, onPlace); - Utils.waitForSyncCallback(cb); + Async.waitForSyncCallback(cb); return failed; }, diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 8242ae884e8..24ec294ec09 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -46,6 +46,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/ext/Observers.js"); Cu.import("resource://services-sync/ext/Preferences.js"); @@ -452,7 +453,7 @@ Resource.prototype = { // is never called directly, but is used by the high-level // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. _request: function Res__request(action, data) { - let cb = Utils.makeSyncCallback(); + let cb = Async.makeSyncCallback(); function callback(error, ret) { if (error) cb.throw(error); @@ -462,7 +463,7 @@ Resource.prototype = { // The channel listener might get a failure code try { this._doRequest(action, data, callback); - return Utils.waitForSyncCallback(cb); + return Async.waitForSyncCallback(cb); } catch(ex) { // Combine the channel stack with this request stack. Need to create // a new error object for that. diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 1c8f3079bb0..5f5f9b9998d 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -54,12 +54,6 @@ Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); -// Constants for makeSyncCallback, waitForSyncCallback -const CB_READY = {}; -const CB_COMPLETE = {}; -const CB_FAIL = {}; -const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR; - /* * Utility functions */ @@ -194,60 +188,6 @@ let Utils = { } }, - // Prototype for mozIStorageCallback, used in queryAsync below. - // This allows us to define the handle* functions just once rather - // than on every queryAsync invocation. - _storageCallbackPrototype: { - results: null, - - // These are set by queryAsync. - names: null, - syncCb: null, - - handleResult: function handleResult(results) { - if (!this.names) { - return; - } - if (!this.results) { - this.results = []; - } - let row; - while ((row = results.getNextRow()) != null) { - let item = {}; - for each (name in this.names) { - item[name] = row.getResultByName(name); - } - this.results.push(item); - } - }, - handleError: function handleError(error) { - this.syncCb.throw(error); - }, - handleCompletion: function handleCompletion(reason) { - - // If we got an error, handleError will also have been called, so don't - // call the callback! We never cancel statements, so we don't need to - // address that quandary. - if (reason == REASON_ERROR) - return; - - // If we were called with column names but didn't find any results, - // the calling code probably still expects an array as a return value. - if (this.names && !this.results) { - this.results = []; - } - this.syncCb(this.results); - } - }, - - queryAsync: function(query, names) { - // Synchronously asyncExecute fetching all results by name - let storageCallback = {names: names, - syncCb: Utils.makeSyncCallback()}; - storageCallback.__proto__ = Utils._storageCallbackPrototype; - query.executeAsync(storageCallback); - return Utils.waitForSyncCallback(storageCallback.syncCb); - }, /* * Partition the input array into an array of arrays. Return a generator. @@ -1235,12 +1175,6 @@ let Utils = { return false; }, - /** - * Helpers for making asynchronous calls within a synchronous API possible. - * - * If you value your sanity, do not look closely at the following functions. - */ - /** * Check if the app is ready (not quitting) */ @@ -1253,57 +1187,6 @@ let Utils = { }); // In the common case, checkAppReady just returns true return (Utils.checkAppReady = function() true)(); - }, - - /** - * Create a sync callback that remembers state like whether it's been called - */ - makeSyncCallback: function makeSyncCallback() { - // The main callback remembers the value it's passed and that it got data - let onComplete = function onComplete(data) { - onComplete.state = CB_COMPLETE; - onComplete.value = data; - }; - - // Initialize private callback data to prepare to be called - onComplete.state = CB_READY; - onComplete.value = null; - - // Allow an alternate callback to trigger an exception to be thrown - onComplete.throw = function onComplete_throw(data) { - onComplete.state = CB_FAIL; - onComplete.value = data; - - // Cause the caller to get an exception and stop execution - throw data; - }; - - return onComplete; - }, - - /** - * Wait for a sync callback to finish - */ - waitForSyncCallback: function waitForSyncCallback(callback) { - // Grab the current thread so we can make it give up priority - let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; - - // Keep waiting until our callback is triggered unless the app is quitting - while (Utils.checkAppReady() && callback.state == CB_READY) { - thread.processNextEvent(true); - } - - // Reset the state of the callback to prepare for another call - let state = callback.state; - callback.state = CB_READY; - - // Throw the value the callback decided to fail with - if (state == CB_FAIL) { - throw callback.value; - } - - // Return the value passed to the callback - return callback.value; } }; diff --git a/services/sync/tests/unit/test_async_helpers.js b/services/sync/tests/unit/test_async_helpers.js new file mode 100644 index 00000000000..efe7d8af704 --- /dev/null +++ b/services/sync/tests/unit/test_async_helpers.js @@ -0,0 +1,297 @@ +Cu.import("resource://services-sync/async.js"); + +function chain(fs) { + fs.reduce(function (prev, next) next.bind(this, prev), + run_next_test)(); +} + +// barrieredCallbacks. +add_test(function test_barrieredCallbacks() { + let s1called = false; + let s2called = false; + + function reset() { + _(" > reset."); + s1called = s2called = false; + } + function succeed1(err, result) { + _(" > succeed1."); + s1called = true; + } + function succeed2(err, result) { + _(" > succeed2."); + s2called = true; + } + function fail1(err, result) { + _(" > fail1."); + return "failed"; + } + function throw1(err, result) { + _(" > throw1."); + throw "Aieeee!"; + } + + function doneSequential(next, err) { + _(" > doneSequential."); + do_check_eq(err, "failed"); + do_check_true(s1called); + do_check_true(s2called); + next(); + } + function doneFailFirst(next, err) { + _(" > doneFailFirst."); + do_check_eq(err, "failed"); + do_check_false(s1called); + do_check_false(s2called); + next(); + } + function doneOnlySucceed(next, err) { + _(" > doneOnlySucceed."); + do_check_true(!err); + do_check_true(s1called); + do_check_true(s2called); + next(); + } + function doneThrow(next, err) { + _(" > doneThrow."); + do_check_eq(err, "Aieeee!"); + do_check_true(s1called); + do_check_false(s2called); + next(); + } + + function sequence_test(label, parts, end) { + return function (next) { + _("Sequence test '" + label + "':"); + reset(); + for (let cb in Async.barrieredCallbacks(parts, end.bind(this, next))) + cb(); + }; + } + + chain( + [sequence_test("failFirst", + [fail1, succeed1, succeed2], + doneFailFirst), + + sequence_test("sequentially", + [succeed1, succeed2, fail1], + doneSequential), + + sequence_test("onlySucceed", + [succeed1, succeed2], + doneOnlySucceed), + + sequence_test("throw", + [succeed1, throw1, succeed2], + doneThrow)]); + +}); + +add_test(function test_empty_barrieredCallbacks() { + let err; + try { + Async.barrieredCallbacks([], function (err) { }).next(); + } catch (ex) { + err = ex; + } + _("err is " + err); + do_check_true(err instanceof StopIteration); + run_next_test(); +}); + +add_test(function test_no_output_barrieredCallbacks() { + let err; + try { + Async.barrieredCallbacks([function (x) {}], null); + } catch (ex) { + err = ex; + } + do_check_eq(err, "No output callback provided to barrieredCallbacks."); + run_next_test(); +}); + +add_test(function test_serially() { + let called = {}; + let i = 1; + function reset() { + called = {}; + i = 0; + } + + function f(x, cb) { + called[x] = ++i; + cb(null, x); + } + + function err_on(expected) { + return function (err, result, context) { + if (err) { + return err; + } + if (result == expected) { + return expected; + } + _("Got " + result + ", passing."); + }; + } + + // Fail in the middle. + reset(); + Async.serially(["a", "b", "d"], f, err_on("b"), function (err) { + do_check_eq(1, called["a"]); + do_check_eq(2, called["b"]); + do_check_false(!!called["d"]); + do_check_eq(err, "b"); + + // Don't fail. + reset(); + Async.serially(["a", "d", "b"], f, err_on("x"), function (err) { + do_check_eq(1, called["a"]); + do_check_eq(3, called["b"]); + do_check_eq(2, called["d"]); + do_check_false(!!err); + + // Empty inputs. + reset(); + Async.serially([], f, err_on("a"), function (err) { + do_check_false(!!err); + + reset(); + Async.serially(undefined, f, err_on("a"), function (err) { + do_check_false(!!err); + run_next_test(); + }); + }); + }); + }); +}); + +add_test(function test_countedCallback() { + let error = null; + let output = null; + let context = null; + let counter = 0; + function cb(err, result, ctx) { + counter++; + output = result; + error = err; + context = ctx; + if (err == "error!") + return "Oh dear."; + } + + let c1; + + c1 = Async.countedCallback(cb, 3, function (err) { + do_check_eq(2, counter); + do_check_eq("error!", error); + do_check_eq(2, output); + do_check_eq("b", context); + do_check_eq(err, "Oh dear."); + + // If we call the counted callback again (once this output function is + // done, that is), then the component callback is not invoked. + Utils.delay(function () { + _("Don't expect component callback."); + c1("not", "running", "now"); + do_check_eq(2, counter); + do_check_eq("error!", error); + do_check_eq(2, output); + do_check_eq("b", context); + run_next_test(); + }, 1, this); + }); + + c1(1, "foo", "a"); + do_check_eq(1, counter); + do_check_eq(1, error); + do_check_eq("foo", output); + do_check_eq("a", context); + + c1("error!", 2, "b"); + // Subsequent checks must now take place inside the 'done' callback... read + // above! +}); + +add_test(function test_finallyCallback() { + let fnCalled = false; + let cbCalled = false; + let error = undefined; + + function reset() { + fnCalled = cbCalled = false; + error = undefined; + } + + function fn(arg) { + do_check_false(!!arg); + fnCalled = true; + } + + function fnThrow(arg) { + do_check_false(!!arg); + fnCalled = true; + throw "Foo"; + } + + function cb(next, err) { + _("Called with " + err); + cbCalled = true; + error = err; + next(); + } + + function allGood(next) { + reset(); + let callback = cb.bind(this, function() { + do_check_true(fnCalled); + do_check_true(cbCalled); + do_check_false(!!error); + next(); + }); + Async.finallyCallback(callback, fn)(null); + } + + function inboundErr(next) { + reset(); + let callback = cb.bind(this, function() { + do_check_true(fnCalled); + do_check_true(cbCalled); + do_check_eq(error, "Baz"); + next(); + }); + Async.finallyCallback(callback, fn)("Baz"); + } + + function throwsNoErr(next) { + reset(); + let callback = cb.bind(this, function() { + do_check_true(fnCalled); + do_check_true(cbCalled); + do_check_eq(error, "Foo"); + next(); + }); + Async.finallyCallback(callback, fnThrow)(null); + } + + function throwsOverrulesErr(next) { + reset(); + let callback = cb.bind(this, function() { + do_check_true(fnCalled); + do_check_true(cbCalled); + do_check_eq(error, "Foo"); + next(); + }); + Async.finallyCallback(callback, fnThrow)("Bar"); + } + + chain([throwsOverrulesErr, + throwsNoErr, + inboundErr, + allGood]); +}); + +function run_test() { + run_next_test(); +} diff --git a/services/sync/tests/unit/test_utils_queryAsync.js b/services/sync/tests/unit/test_async_querySpinningly.js similarity index 57% rename from services/sync/tests/unit/test_utils_queryAsync.js rename to services/sync/tests/unit/test_async_querySpinningly.js index 234be1b1c13..6955af31dae 100644 --- a/services/sync/tests/unit/test_utils_queryAsync.js +++ b/services/sync/tests/unit/test_async_querySpinningly.js @@ -1,5 +1,5 @@ -_("Make sure queryAsync will synchronously fetch rows for a query asyncly"); -Cu.import("resource://services-sync/util.js"); +_("Make sure querySpinningly will synchronously fetch rows for a query asyncly"); +Cu.import("resource://services-sync/async.js"); const SQLITE_CONSTRAINT_VIOLATION = 19; // http://www.sqlite.org/c3ref/c_abort.html @@ -15,62 +15,62 @@ function run_test() { do_check_false(isAsync); _("Empty out the formhistory table"); - let r0 = Utils.queryAsync(c("DELETE FROM moz_formhistory")); + let r0 = Async.querySpinningly(c("DELETE FROM moz_formhistory")); do_check_eq(r0, null); _("Make sure there's nothing there"); - let r1 = Utils.queryAsync(c("SELECT 1 FROM moz_formhistory")); + let r1 = Async.querySpinningly(c("SELECT 1 FROM moz_formhistory")); do_check_eq(r1, null); _("Insert a row"); - let r2 = Utils.queryAsync(c("INSERT INTO moz_formhistory (fieldname, value) VALUES ('foo', 'bar')")); + let r2 = Async.querySpinningly(c("INSERT INTO moz_formhistory (fieldname, value) VALUES ('foo', 'bar')")); do_check_eq(r2, null); _("Request a known value for the one row"); - let r3 = Utils.queryAsync(c("SELECT 42 num FROM moz_formhistory"), ["num"]); + let r3 = Async.querySpinningly(c("SELECT 42 num FROM moz_formhistory"), ["num"]); do_check_eq(r3.length, 1); do_check_eq(r3[0].num, 42); _("Get multiple columns"); - let r4 = Utils.queryAsync(c("SELECT fieldname, value FROM moz_formhistory"), ["fieldname", "value"]); + let r4 = Async.querySpinningly(c("SELECT fieldname, value FROM moz_formhistory"), ["fieldname", "value"]); do_check_eq(r4.length, 1); do_check_eq(r4[0].fieldname, "foo"); do_check_eq(r4[0].value, "bar"); _("Get multiple columns with a different order"); - let r5 = Utils.queryAsync(c("SELECT fieldname, value FROM moz_formhistory"), ["value", "fieldname"]); + let r5 = Async.querySpinningly(c("SELECT fieldname, value FROM moz_formhistory"), ["value", "fieldname"]); do_check_eq(r5.length, 1); do_check_eq(r5[0].fieldname, "foo"); do_check_eq(r5[0].value, "bar"); _("Add multiple entries (sqlite doesn't support multiple VALUES)"); - let r6 = Utils.queryAsync(c("INSERT INTO moz_formhistory (fieldname, value) SELECT 'foo', 'baz' UNION SELECT 'more', 'values'")); + let r6 = Async.querySpinningly(c("INSERT INTO moz_formhistory (fieldname, value) SELECT 'foo', 'baz' UNION SELECT 'more', 'values'")); do_check_eq(r6, null); _("Get multiple rows"); - let r7 = Utils.queryAsync(c("SELECT fieldname, value FROM moz_formhistory WHERE fieldname = 'foo'"), ["fieldname", "value"]); + let r7 = Async.querySpinningly(c("SELECT fieldname, value FROM moz_formhistory WHERE fieldname = 'foo'"), ["fieldname", "value"]); do_check_eq(r7.length, 2); do_check_eq(r7[0].fieldname, "foo"); do_check_eq(r7[1].fieldname, "foo"); _("Make sure updates work"); - let r8 = Utils.queryAsync(c("UPDATE moz_formhistory SET value = 'updated' WHERE fieldname = 'more'")); + let r8 = Async.querySpinningly(c("UPDATE moz_formhistory SET value = 'updated' WHERE fieldname = 'more'")); do_check_eq(r8, null); _("Get the updated"); - let r9 = Utils.queryAsync(c("SELECT value, fieldname FROM moz_formhistory WHERE fieldname = 'more'"), ["fieldname", "value"]); + let r9 = Async.querySpinningly(c("SELECT value, fieldname FROM moz_formhistory WHERE fieldname = 'more'"), ["fieldname", "value"]); do_check_eq(r9.length, 1); do_check_eq(r9[0].fieldname, "more"); do_check_eq(r9[0].value, "updated"); _("Grabbing fewer fields than queried is fine"); - let r10 = Utils.queryAsync(c("SELECT value, fieldname FROM moz_formhistory"), ["fieldname"]); + let r10 = Async.querySpinningly(c("SELECT value, fieldname FROM moz_formhistory"), ["fieldname"]); do_check_eq(r10.length, 3); _("Generate an execution error"); let r11, except, query = c("INSERT INTO moz_formhistory (fieldname, value) VALUES ('one', NULL)"); try { - r11 = Utils.queryAsync(query); + r11 = Async.querySpinningly(query); } catch(e) { except = e; } @@ -78,7 +78,7 @@ function run_test() { do_check_eq(except.result, SQLITE_CONSTRAINT_VIOLATION); _("Cleaning up"); - Utils.queryAsync(c("DELETE FROM moz_formhistory")); + Async.querySpinningly(c("DELETE FROM moz_formhistory")); _("Make sure the timeout got to run before this function ends"); do_check_true(isAsync); diff --git a/services/sync/tests/unit/test_bookmark_engine.js b/services/sync/tests/unit/test_bookmark_engine.js index c9a338bb0b7..79878469626 100644 --- a/services/sync/tests/unit/test_bookmark_engine.js +++ b/services/sync/tests/unit/test_bookmark_engine.js @@ -2,6 +2,7 @@ Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/engines/bookmarks.js"); Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/service.js"); diff --git a/services/sync/tests/unit/test_history_store.js b/services/sync/tests/unit/test_history_store.js index 25a21206847..8eb142bd542 100644 --- a/services/sync/tests/unit/test_history_store.js +++ b/services/sync/tests/unit/test_history_store.js @@ -1,5 +1,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-sync/engines/history.js"); +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); const TIMESTAMP1 = (Date.now() - 103406528) * 1000; @@ -166,7 +167,7 @@ add_test(function test_invalid_records() { + "(url, title, rev_host, visit_count, last_visit_date) " + "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")"; let stmt = PlacesUtils.history.DBConnection.createAsyncStatement(query); - let result = Utils.queryAsync(stmt); + let result = Async.querySpinningly(stmt); do_check_eq([id for (id in store.getAllIDs())].length, 4); _("Make sure we report records with invalid URIs."); diff --git a/services/sync/tests/unit/test_places_guid_downgrade.js b/services/sync/tests/unit/test_places_guid_downgrade.js index 080f51bd566..dbfc43f802a 100644 --- a/services/sync/tests/unit/test_places_guid_downgrade.js +++ b/services/sync/tests/unit/test_places_guid_downgrade.js @@ -1,3 +1,4 @@ +Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/engines/history.js"); @@ -104,11 +105,11 @@ function test_history_guids() { "SELECT id FROM moz_places WHERE guid = :guid"); stmt.params.guid = fxguid; - let result = Utils.queryAsync(stmt, ["id"]); + let result = Async.querySpinningly(stmt, ["id"]); do_check_eq(result.length, 1); stmt.params.guid = tbguid; - result = Utils.queryAsync(stmt, ["id"]); + result = Async.querySpinningly(stmt, ["id"]); do_check_eq(result.length, 1); _("History: Verify GUIDs weren't added to annotations."); @@ -116,11 +117,11 @@ function test_history_guids() { "SELECT a.content AS guid FROM moz_annos a WHERE guid = :guid"); stmt.params.guid = fxguid; - result = Utils.queryAsync(stmt, ["guid"]); + result = Async.querySpinningly(stmt, ["guid"]); do_check_eq(result.length, 0); stmt.params.guid = tbguid; - result = Utils.queryAsync(stmt, ["guid"]); + result = Async.querySpinningly(stmt, ["guid"]); do_check_eq(result.length, 0); } @@ -147,12 +148,12 @@ function test_bookmark_guids() { "SELECT id FROM moz_bookmarks WHERE guid = :guid"); stmt.params.guid = fxguid; - let result = Utils.queryAsync(stmt, ["id"]); + let result = Async.querySpinningly(stmt, ["id"]); do_check_eq(result.length, 1); do_check_eq(result[0].id, fxid); stmt.params.guid = tbguid; - result = Utils.queryAsync(stmt, ["id"]); + result = Async.querySpinningly(stmt, ["id"]); do_check_eq(result.length, 1); do_check_eq(result[0].id, tbid); @@ -161,11 +162,11 @@ function test_bookmark_guids() { "SELECT a.content AS guid FROM moz_items_annos a WHERE guid = :guid"); stmt.params.guid = fxguid; - result = Utils.queryAsync(stmt, ["guid"]); + result = Async.querySpinningly(stmt, ["guid"]); do_check_eq(result.length, 0); stmt.params.guid = tbguid; - result = Utils.queryAsync(stmt, ["guid"]); + result = Async.querySpinningly(stmt, ["guid"]); do_check_eq(result.length, 0); } diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini index e2a1405d585..92cb6c47304 100644 --- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -4,6 +4,8 @@ tail = [test_Observers.js] [test_Preferences.js] +[test_async_helpers.js] +[test_async_querySpinningly.js] [test_auth_manager.js] [test_bookmark_batch_fail.js] [test_bookmark_engine.js] @@ -93,7 +95,6 @@ tail = [test_utils_notify.js] [test_utils_passphrase.js] [test_utils_pbkdf2.js] -[test_utils_queryAsync.js] [test_utils_sha1.js] [test_utils_sha1hmac.js] [test_utils_sha256HMAC.js]