author | Richard Newman <rnewman@mozilla.com> |
Wed, 01 Jun 2011 18:20:09 -0700 | |
changeset 70447 | cadc21488ae788341eeb3695f4fa6b30c834330f |
parent 70426 | a4107cfe6b211679caf99d37a50f4084a755dc18 (current diff) |
parent 70446 | 17afae19309183ed6b66242f99acde905738a1e1 (diff) |
child 70448 | b3760bf020265db8b97f12ff0f723642570780e6 |
push id | 20321 |
push user | rnewman@mozilla.com |
push date | Thu, 02 Jun 2011 01:21:25 +0000 |
treeherder | mozilla-central@cadc21488ae7 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 7.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
services/sync/tests/unit/test_utils_queryAsync.js | file | annotate | diff | comparison | revisions |
--- a/browser/base/content/browser-syncui.js +++ b/browser/base/content/browser-syncui.js @@ -323,17 +323,17 @@ let gSyncUI = { if (!lastSync || this._needsSetup()) { syncButton.removeAttribute("tooltiptext"); return; } // Show the day-of-week and time (HH:MM) of last sync let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); let lastSyncLabel = - this._stringBundle.formatStringFromName("lastSync.label", [lastSyncDate], 1); + this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1); syncButton.setAttribute("tooltiptext", lastSyncLabel); }, _onSyncEnd: function SUI__onSyncEnd(success) { let title = this._stringBundle.GetStringFromName("error.sync.title"); if (!success) { if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
--- a/browser/base/content/syncAddDevice.xul +++ b/browser/base/content/syncAddDevice.xul @@ -49,16 +49,17 @@ %syncBrandDTD; %syncSetupDTD; ]> <wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" id="wizard" title="&addDevice.title.label;" windowtype="Sync:AddDevice" + persist="screenX screenY" onwizardnext="return gSyncAddDevice.onWizardAdvance();" onwizardback="return gSyncAddDevice.onWizardBack();" onwizardcancel="gSyncAddDevice.onWizardCancel();" onload="gSyncAddDevice.init();"> <script type="application/javascript" src="chrome://browser/content/syncAddDevice.js"/> <script type="application/javascript"
--- a/browser/base/content/syncGenericChange.xul +++ b/browser/base/content/syncGenericChange.xul @@ -50,16 +50,17 @@ %brandDTD; %syncBrandDTD; %syncSetupDTD; ]> <wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" id="change-dialog" windowtype="Weave:ChangeSomething" + persist="screenX screenY" onwizardnext="Change.onLoad()" onwizardfinish="return Change.onDialogAccept();"> <script type="application/javascript" src="chrome://browser/content/syncGenericChange.js"/> <script type="application/javascript" src="chrome://browser/content/syncUtils.js"/> <script type="application/javascript"
--- a/browser/base/content/syncKey.xhtml +++ b/browser/base/content/syncKey.xhtml @@ -46,16 +46,17 @@ %syncKeyDTD; <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" > %globalDTD; ]> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>&syncKey.page.title;</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="robots" content="noindex"/> <style type="text/css"> #synckey { font-size: 150% } footer { font-size: 70% } # Bug 575675: Need to have an a:visited rule in a chrome document. a:visited { color: purple; } </style> </head>
--- a/browser/base/content/syncQuota.xul +++ b/browser/base/content/syncQuota.xul @@ -45,16 +45,17 @@ <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> <!ENTITY % syncQuotaDTD SYSTEM "chrome://browser/locale/syncQuota.dtd"> %brandDTD; %syncBrandDTD; %syncQuotaDTD; ]> <dialog id="quotaDialog" windowtype="Sync:ViewQuota" + persist="screenX screenY width height" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" onload="gSyncQuota.init()" buttons="accept,cancel" title=""a.dialogTitle.label;" ondialogcancel="return true;" ondialogaccept="return gSyncQuota.onAccept();">
--- a/browser/base/content/syncSetup.xul +++ b/browser/base/content/syncSetup.xul @@ -49,16 +49,17 @@ <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> <!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> %brandDTD; %syncBrandDTD; %syncSetupDTD; ]> <wizard id="accountSetup" title="&accountSetupTitle.label;" windowtype="Weave:AccountSetup" + persist="screenX screenY" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" onwizardnext="return gSyncSetup.onWizardAdvance()" onwizardback="return gSyncSetup.onWizardBack()" onwizardfinish="gSyncSetup.onWizardFinish()" onwizardcancel="gSyncSetup.onWizardCancel()" onload="gSyncSetup.init()">
--- a/mobile/chrome/content/sync.js +++ b/mobile/chrome/content/sync.js @@ -432,32 +432,32 @@ let WeaveGlue = { if (Weave.Service.locked) { connect.firstChild.disabled = true; sync.firstChild.disabled = true; if (aTopic == "weave:service:login:start") connect.setAttribute("title", self._bundle.GetStringFromName("connecting.label")); if (aTopic == "weave:service:sync:start") - sync.setAttribute("title", self._bundle.GetStringFromName("lastSyncInProgress.label")); + sync.setAttribute("title", self._bundle.GetStringFromName("lastSyncInProgress2.label")); } else { connect.firstChild.disabled = false; sync.firstChild.disabled = false; } }, 0, this); // Dynamically generate some strings let accountStr = this._bundle.formatStringFromName("account.label", [Weave.Service.account], 1); disconnect.setAttribute("title", accountStr); // Show the day-of-week and time (HH:MM) of last sync let lastSync = Weave.Svc.Prefs.get("lastSync"); if (lastSync != null) { let syncDate = new Date(lastSync).toLocaleFormat("%a %R"); - let dateStr = this._bundle.formatStringFromName("lastSync.label", [syncDate], 1); + let dateStr = this._bundle.formatStringFromName("lastSync2.label", [syncDate], 1); sync.setAttribute("title", dateStr); } // Show what went wrong with login if necessary if (aTopic == "weave:service:login:error") connect.setAttribute("desc", Weave.Utils.getErrorString(Weave.Status.login)); else connect.removeAttribute("desc");
--- a/mobile/locales/en-US/chrome/sync.properties +++ b/mobile/locales/en-US/chrome/sync.properties @@ -1,13 +1,13 @@ # Mobile Sync # %S is the date and time at which the last sync successfully completed -lastSync.label=Last update: %S -lastSyncInProgress.label=Last update: in progress… +lastSync2.label=Last sync: %S +lastSyncInProgress2.label=Last sync: in progress… # %S is the username logged in account.label=Account: %S notconnected.label=Not connected connecting.label=Connecting… notificationDisconnect.label=Your Firefox Sync account has been removed notificationDisconnect.button=Undo
--- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,19 +1,13 @@ # %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android) client.name2 = %1$S's %2$S on %3$S # %S is the date and time at which the last sync successfully completed -lastSync.label = Last Update: %S -lastSyncInProgress.label = Last Update: in progress… - -# %S is the username logged in -connected.label = Connected: %S -disconnected.label = Disconnected -connecting.label = Connecting… +lastSync2.label = Last sync: %S mobile.label = Mobile Bookmarks remote.pending.label = Remote tabs are being synced… remote.missing.label = Sync your other computers again to access their tabs remote.opened.label = All remote tabs are already open remote.notification.label = Recent desktop tabs will be available once they sync
new file mode 100644 --- /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 <rnewman@mozilla.com> + * + * 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); + }, +};
--- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -41,16 +41,17 @@ const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine', 'Tracker', 'Store']; const Cc = Components.classes; 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"); Cu.import("resource://services-sync/identity.js"); Cu.import("resource://services-sync/log4moz.js"); Cu.import("resource://services-sync/resource.js"); Cu.import("resource://services-sync/util.js"); @@ -195,20 +196,20 @@ function Store(name) { XPCOMUtils.defineLazyGetter(this, "_timer", function() { return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); }); } 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) { let failed = []; for each (let record in records) { try { this.applyIncoming(record); } catch (ex) { @@ -412,16 +413,17 @@ Engine.prototype = { wipeClient: function Engine_wipeClient() { this._notify("wipe-client", this.name, this._wipeClient)(); } }; function SyncEngine(name) { Engine.call(this, name || "SyncEngine"); this.loadToFetch(); + this.loadPreviousFailed(); } // Enumeration to define approaches to handling bad records. // Attached to the constructor to allow use as a kind of static enumeration. SyncEngine.kRecoveryStrategy = { ignore: "ignore", retry: "retry", error: "error" @@ -478,32 +480,58 @@ SyncEngine.prototype = { this._log.debug("Resetting " + this.name + " last sync time"); Svc.Prefs.reset(this.name + ".lastSync"); Svc.Prefs.set(this.name + ".lastSync", "0"); this.lastSyncLocal = 0; }, get toFetch() this._toFetch, set toFetch(val) { + // Coerce the array to a string for more efficient comparison. + if (val + "" == this._toFetch) { + return; + } this._toFetch = val; Utils.delay(function () { Utils.jsonSave("toFetch/" + this.name, this, val); }, 0, this, "_toFetchDelay"); }, loadToFetch: function loadToFetch() { // Initialize to empty if there's no file this._toFetch = []; Utils.jsonLoad("toFetch/" + this.name, this, function(toFetch) { if (toFetch) { this._toFetch = toFetch; } }); }, + get previousFailed() this._previousFailed, + set previousFailed(val) { + // Coerce the array to a string for more efficient comparison. + if (val + "" == this._previousFailed) { + return; + } + this._previousFailed = val; + Utils.delay(function () { + Utils.jsonSave("failed/" + this.name, this, val); + }, 0, this, "_previousFailedDelay"); + }, + + loadPreviousFailed: function loadPreviousFailed() { + // Initialize to empty if there's no file + this._previousFailed = []; + Utils.jsonLoad("failed/" + this.name, this, function(previousFailed) { + if (previousFailed) { + this._previousFailed = previousFailed; + } + }); + }, + /* * lastSyncLocal is a timestamp in local time. */ get lastSyncLocal() { return parseInt(Svc.Prefs.get(this.name + ".lastSyncLocal", "0"), 10); }, set lastSyncLocal(value) { // Store as a string because pref can only store C longs as numbers. @@ -613,38 +641,45 @@ SyncEngine.prototype = { let isMobile = (Svc.Prefs.get("client.type") == "mobile"); if (isMobile) { batchSize = MOBILE_BATCH_SIZE; } newitems.newer = this.lastSync; newitems.full = true; newitems.limit = batchSize; - - let count = {applied: 0, failed: 0, reconciled: 0}; + + // applied => number of items that should be applied. + // failed => number of items that failed in this sync. + // newFailed => number of items that failed for the first time in this sync. + // reconciled => number of items that were reconciled. + let count = {applied: 0, failed: 0, newFailed: 0, reconciled: 0}; let handled = []; let applyBatch = []; let failed = []; - let fetchBatch = this.toFetch; + let failedInPreviousSync = this.previousFailed; + let fetchBatch = Utils.arrayUnion(this.toFetch, failedInPreviousSync); + // Reset previousFailed for each sync since previously failed items may not fail again. + this.previousFailed = []; function doApplyBatch() { this._tracker.ignoreAll = true; failed = failed.concat(this._store.applyIncomingBatch(applyBatch)); this._tracker.ignoreAll = false; applyBatch = []; } function doApplyBatchAndPersistFailed() { // Apply remaining batch. if (applyBatch.length) { doApplyBatch.call(this); } // Persist failed items so we refetch them. if (failed.length) { - this.toFetch = Utils.arrayUnion(failed, this.toFetch); + this.previousFailed = Utils.arrayUnion(failed, this.previousFailed); count.failed += failed.length; this._log.debug("Records that failed to apply: " + failed); failed = []; } } // Not binding this method to 'this' for performance reasons. It gets // called for every incoming record. @@ -782,37 +817,41 @@ SyncEngine.prototype = { if (!resp.success) { resp.failureCode = ENGINE_DOWNLOAD_FAIL; throw resp; } // This batch was successfully applied. Not using // doApplyBatchAndPersistFailed() here to avoid writing toFetch twice. fetchBatch = fetchBatch.slice(batchSize); - let newToFetch = Utils.arraySub(this.toFetch, newitems.ids); - this.toFetch = Utils.arrayUnion(newToFetch, failed); - count.failed += failed.length; - this._log.debug("Records that failed to apply: " + failed); + this.toFetch = Utils.arraySub(this.toFetch, newitems.ids); + this.previousFailed = Utils.arrayUnion(this.previousFailed, failed); + if (failed.length) { + count.failed += failed.length; + this._log.debug("Records that failed to apply: " + failed); + } failed = []; if (this.lastSync < this.lastModified) { this.lastSync = this.lastModified; } } // Apply remaining items. doApplyBatchAndPersistFailed.call(this); - if (count.failed) { + count.newFailed = Utils.arraySub(this.previousFailed, failedInPreviousSync).length; + if (count.newFailed) { // Notify observers if records failed to apply. Pass the count object // along so that they can make an informed decision on what to do. Observers.notify("weave:engine:sync:apply-failed", count, this.name); } this._log.info(["Records:", count.applied, "applied,", count.failed, "failed to apply,", + count.newFailed, "newly failed to apply,", count.reconciled, "reconciled."].join(" ")); }, /** * Find a GUID of an item that is a duplicate of the incoming item but happens * to have a different GUID * * @return GUID of the similar item; falsy otherwise @@ -872,17 +911,17 @@ SyncEngine.prototype = { if (item.id in this._modified) { // If the incoming and local changes are the same, skip if (this._isEqual(item)) { delete this._modified[item.id]; return false; } // Records differ so figure out which to take - let recordAge = Resource.serverTime - item.modified; + let recordAge = AsyncResource.serverTime - item.modified; let localAge = Date.now() / 1000 - this._modified[item.id]; this._log.trace("Record age vs local age: " + [recordAge, localAge]); // Apply the record if the record is newer (server wins) return recordAge < localAge; } this._log.trace("Reconcile step 2: Check for updates"); @@ -1046,16 +1085,17 @@ SyncEngine.prototype = { this._log.debug("Failed test decrypt: " + Utils.exceptionStr(ex)); } return canDecrypt; }, _resetClient: function SyncEngine__resetClient() { this.resetLastSync(); + this.previousFailed = []; this.toFetch = []; }, wipeServer: function wipeServer() { new Resource(this.engineURL).delete(); this._resetClient(); },
--- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -63,16 +63,17 @@ const ANNOS_TO_TRACK = [DESCRIPTION_ANNO const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; const FOLDER_SORTINDEX = 1000000; 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. function PlacesItem(collection, id, type) { CryptoWrapper.call(this, collection, id); this.type = type || "item"; } @@ -927,17 +928,17 @@ BookmarksStore.prototype = { "WHERE parent = :parent " + "ORDER BY position"); }, _childGUIDsCols: ["item_id", "guid"], _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; } // A GUID hasn't been assigned to this item yet, do this now. return this.GUIDForId(row.item_id); }, this); }, @@ -1070,17 +1071,17 @@ BookmarksStore.prototype = { // Some helper functions to handle GUIDs _setGUID: function _setGUID(id, guid) { if (!guid) guid = Utils.makeGUID(); let stmt = this._setGUIDStm; stmt.params.guid = guid; stmt.params.item_id = id; - Utils.queryAsync(stmt); + Async.querySpinningly(stmt); return guid; }, get _guidForIdStm() { return this._getStmt( "SELECT guid " + "FROM moz_bookmarks " + "WHERE id = :item_id"); @@ -1091,17 +1092,17 @@ BookmarksStore.prototype = { let special = kSpecialIds.specialGUIDForId(id); if (special) return special; let stmt = this._guidForIdStm; 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; // Give the uri a GUID if it doesn't have one return this._setGUID(id); }, get _idForGUIDStm() { @@ -1117,17 +1118,17 @@ BookmarksStore.prototype = { idForGUID: function idForGUID(guid, noCreate) { if (kSpecialIds.isSpecialGUID(guid)) return kSpecialIds.specialIdForGUID(guid, !noCreate); let stmt = this._idForGUIDStm; // 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); // Here's the one we care about: the first. let result = results[0]; if (!result) return -1; @@ -1144,17 +1145,17 @@ BookmarksStore.prototype = { // unvisited bookmark let index = 0; if (record.parentid == "toolbar") index += 150; // 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; } return index; }, _getChildren: function BStore_getChildren(guid, items) {
--- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -38,16 +38,17 @@ const EXPORTED_SYMBOLS = ['FormEngine', const Cc = Components.classes; const Ci = Components.interfaces; 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"); const FORMS_TTL = 5184000; // 60 days function FormRec(collection, id) { CryptoWrapper.call(this, collection, id); @@ -67,36 +68,36 @@ let FormWrapper = { getAllEntries: function getAllEntries() { // Sort by (lastUsed - minLast) / (maxLast - minLast) * timesUsed / maxTimes let query = Svc.Form.DBConnection.createAsyncStatement( "SELECT fieldname name, value FROM moz_formhistory " + "ORDER BY 1.0 * (lastUsed - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / " + "((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) { // Query for the provided entry let getQuery = Svc.Form.DBConnection.createAsyncStatement( "SELECT guid FROM moz_formhistory " + "WHERE fieldname = :name AND value = :value"); getQuery.params.name = name; 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... // Might as well just return. this._log.warn("GUID query returned " + item + "; turn on Trace logging for details."); this._log.trace("getGUID(" + JSON.stringify(name) + ", " + JSON.stringify(value) + ") => " + item); return null; @@ -108,34 +109,34 @@ let FormWrapper = { // We need to create a guid for this entry let setQuery = Svc.Form.DBConnection.createAsyncStatement( "UPDATE moz_formhistory SET guid = :guid " + "WHERE fieldname = :name AND value = :value"); let guid = Utils.makeGUID(); setQuery.params.guid = guid; setQuery.params.name = name; setQuery.params.value = value; - Utils.queryAsync(setQuery); + Async.querySpinningly(setQuery); return guid; }, hasGUID: function hasGUID(guid) { 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) { let query = Svc.Form.DBConnection.createAsyncStatement( "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID"); query.params.oldGUID = oldGUID; query.params.newGUID = newGUID; - Utils.queryAsync(query); + Async.querySpinningly(query); } }; function FormEngine() { SyncEngine.call(this, "Forms"); } FormEngine.prototype = {
--- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -46,16 +46,17 @@ const Cr = Components.results; const HISTORY_TTL = 5184000; // 60 days const TOPIC_UPDATEPLACES_COMPLETE = "places-updatePlaces-complete"; 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"); function HistoryRec(collection, id) { CryptoWrapper.call(this, collection, id); } HistoryRec.prototype = { __proto__: CryptoWrapper.prototype, @@ -127,34 +128,34 @@ HistoryStore.prototype = { uri = uri.spec ? uri.spec : uri; if (!guid) guid = Utils.makeGUID(); let stmt = this._setGUIDStm; stmt.params.guid = guid; stmt.params.page_url = uri; - Utils.queryAsync(stmt); + Async.querySpinningly(stmt); return guid; }, get _guidStm() { return this._getStmt( "SELECT guid " + "FROM moz_places " + "WHERE url = :page_url"); }, _guidCols: ["guid"], GUIDForUri: function GUIDForUri(uri, create) { let stm = this._guidStm; 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; // Give the uri a GUID if it doesn't have one if (create) return this.setGUID(uri); }, @@ -183,36 +184,36 @@ HistoryStore.prototype = { "ORDER BY frecency DESC " + "LIMIT :max_results"); }, _allUrlCols: ["url"], // 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) { this.setGUID(this._findURLByGUID(oldID).url, newID); }, getAllIDs: function HistStore_getAllIDs() { // Only get places visited within the last 30 days (30*24*60*60*1000ms) 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; return ids; }, {}); }, applyIncomingBatch: function applyIncomingBatch(records) { @@ -246,29 +247,29 @@ HistoryStore.prototype = { } records.length = k; // truncate array // Nothing to do. if (!records.length) { return failed; } - let cb = Utils.makeSyncCallback(); + let cb = Async.makeSyncCallback(); let onPlace = function onPlace(result, placeInfo) { if (!Components.isSuccessCode(result)) { failed.push(placeInfo.guid); } }; let onComplete = function onComplete(subject, topic, data) { Svc.Obs.remove(TOPIC_UPDATEPLACES_COMPLETE, onComplete); cb(); }; Svc.Obs.add(TOPIC_UPDATEPLACES_COMPLETE, onComplete); this._asyncHistory.updatePlaces(records, onPlace); - Utils.waitForSyncCallback(cb); + Async.waitForSyncCallback(cb); return failed; }, /** * Converts a Sync history record to a mozIPlaceInfo. * * Throws if an invalid record is encountered (invalid URI, etc.), * returns true if the record is to be applied, false otherwise
--- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -174,17 +174,17 @@ TabStore.prototype = { return allTabs; }, createRecord: function createRecord(id, collection) { let record = new TabSetRecord(collection, id); record.clientName = Clients.localName; // Don't provide any tabs to compare against and ignore the update later. - if (Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) { + if (Svc.Private && Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) { record.tabs = []; return record; } // Sort tabs in descending-used order to grab the most recently used let tabs = this.getAllTabs(true).sort(function(a, b) { return b.lastUsed - a.lastUsed; }); @@ -211,17 +211,17 @@ TabStore.prototype = { record.tabs = tabs; return record; }, getAllIDs: function TabStore_getAllIds() { // Don't report any tabs if we're in private browsing for first syncs. let ids = {}; - if (Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) + if (Svc.Private && Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) return ids; ids[Clients.localID] = true; return ids; }, wipe: function TabStore_wipe() { this._remoteClients = {}; @@ -328,17 +328,17 @@ TabTracker.prototype = { break; case "private-browsing": if (aData == "enter" && !PBPrefs.get("autostart")) this.modified = false; } }, onTab: function onTab(event) { - if (Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) { + if (Svc.Private && Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) { this._log.trace("Ignoring tab event from private browsing."); return; } this._log.trace("onTab event: " + event.type); this.modified = true; // For pageshow events, only give a partial score bump (~.1)
--- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -41,16 +41,17 @@ const EXPORTED_SYMBOLS = ["Resource", "A "Auth", "BrokenBasicAuthenticator", "BasicAuthenticator", "NoOpAuthenticator"]; const Cc = Components.classes; 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"); Cu.import("resource://services-sync/log4moz.js"); Cu.import("resource://services-sync/util.js"); XPCOMUtils.defineLazyGetter(this, "Auth", function () { return new AuthMgr(); @@ -139,16 +140,21 @@ function AsyncResource(uri) { Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")]; this.uri = uri; this._headers = {}; this._onComplete = Utils.bind2(this, this._onComplete); } AsyncResource.prototype = { _logName: "Net.Resource", + // ** {{{ AsyncResource.serverTime }}} ** + // + // Caches the latest server timestamp (X-Weave-Timestamp header). + serverTime: null, + // The string to use as the base User-Agent in Sync requests. // These strings will look something like // // Firefox/4.0 FxSync/1.8.0.20100101.mobile // // or // // Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop @@ -156,79 +162,79 @@ AsyncResource.prototype = { _userAgent: Services.appinfo.name + "/" + Services.appinfo.version + // Product. " FxSync/" + WEAVE_VERSION + "." + // Sync. Services.appinfo.appBuildID + ".", // Build. // Wait 5 minutes before killing a request. ABORT_TIMEOUT: 300000, - // ** {{{ Resource.authenticator }}} ** + // ** {{{ AsyncResource.authenticator }}} ** // // Getter and setter for the authenticator module // responsible for this particular resource. The authenticator // module may modify the headers to perform authentication // while performing a request for the resource, for example. get authenticator() { if (this._authenticator) return this._authenticator; else return Auth.lookupAuthenticator(this.spec); }, set authenticator(value) { this._authenticator = value; }, - // ** {{{ Resource.headers }}} ** + // ** {{{ AsyncResource.headers }}} ** // // Headers to be included when making a request for the resource. // Note: Header names should be all lower case, there's no explicit // check for duplicates due to case! get headers() { return this.authenticator.onRequest(this._headers); }, set headers(value) { this._headers = value; }, setHeader: function Res_setHeader(header, value) { this._headers[header.toLowerCase()] = value; }, - // ** {{{ Resource.uri }}} ** + // ** {{{ AsyncResource.uri }}} ** // // URI representing this resource. get uri() { return this._uri; }, set uri(value) { if (typeof value == 'string') this._uri = Utils.makeURI(value); else this._uri = value; }, - // ** {{{ Resource.spec }}} ** + // ** {{{ AsyncResource.spec }}} ** // // Get the string representation of the URI. get spec() { if (this._uri) return this._uri.spec; return null; }, - // ** {{{ Resource.data }}} ** + // ** {{{ AsyncResource.data }}} ** // // Get and set the data encapulated in the resource. _data: null, get data() this._data, set data(value) { this._data = value; }, - // ** {{{ Resource._createRequest }}} ** + // ** {{{ AsyncResource._createRequest }}} ** // // This method returns a new IO Channel for requests to be made // through. It is never called directly, only {{{_doRequest}}} uses it // to obtain a request channel. // _createRequest: function Res__createRequest() { let channel = Services.io.newChannel(this.spec, null, null) .QueryInterface(Ci.nsIRequest) @@ -436,38 +442,33 @@ AsyncResource.prototype = { */ function Resource(uri) { AsyncResource.call(this, uri); } Resource.prototype = { __proto__: AsyncResource.prototype, - // ** {{{ Resource.serverTime }}} ** - // - // Caches the latest server timestamp (X-Weave-Timestamp header). - serverTime: null, - // ** {{{ Resource._request }}} ** // // Perform a particular HTTP request on the resource. This method // 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); cb(ret); } // 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. let error = Error(ex.message); error.result = ex.result; let chanStack = []; if (ex.stack) chanStack = ex.stack.trim().split(/\n/).slice(1); @@ -525,50 +526,51 @@ function ChannelListener(onComplete, onP ChannelListener.prototype = { onStartRequest: function Channel_onStartRequest(channel) { this._log.trace("onStartRequest called for channel " + channel + "."); channel.QueryInterface(Ci.nsIHttpChannel); // Save the latest server timestamp when possible. try { - Resource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0; + AsyncResource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0; } catch(ex) {} this._log.trace("onStartRequest: " + channel.requestMethod + " " + channel.URI.spec); this._data = ''; this.delayAbort(); }, onStopRequest: function Channel_onStopRequest(channel, context, status) { // Clear the abort timer now that the channel is done. this.abortTimer.clear(); let success = Components.isSuccessCode(status); + let uri = channel && channel.URI && channel.URI.spec || "<unknown>"; this._log.trace("Channel for " + channel.requestMethod + " " + - channel.URI.spec + ": isSuccessCode(" + status + ")? " + + uri + ": isSuccessCode(" + status + ")? " + success); if (this._data == '') this._data = null; // Throw the failure code and stop execution. Use Components.Exception() // instead of Error() so the exception is QI-able and can be passed across // XPCOM borders while preserving the status code. if (!success) { let message = Components.Exception("", status).name; let error = Components.Exception(message, status); this._onComplete(error); return; } this._log.trace("Channel: flags = " + channel.loadFlags + - ", URI = " + channel.URI.spec + + ", URI = " + uri + ", HTTP success? " + channel.requestSucceeded); this._onComplete(null, this._data); }, onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) { let siStream = Cc["@mozilla.org/scriptableinputstream;1"]. createInstance(Ci.nsIScriptableInputStream); siStream.init(stream);
--- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -49,22 +49,16 @@ Cu.import("resource://services-sync/ext/ Cu.import("resource://services-sync/ext/StringBundle.js"); Cu.import("resource://services-sync/log4moz.js"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); 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 */ let Utils = { /** * Execute an arbitrary number of asynchronous functions one after the * other, passing the callback arguments on to the next one. All functions @@ -189,71 +183,35 @@ let Utils = { return callback.call(thisObj); } finally { if (hasTransaction) { db.commitTransaction(); } } }, - // 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, + /* + * Partition the input array into an array of arrays. Return a generator. + */ + slices: function slices(arr, sliceSize) { + if (!sliceSize || sliceSize <= 0) + throw "Invalid slice size."; - 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); + if (sliceSize > arr.length) { + yield arr; + } else { + let offset = 0; + while (arr.length > offset) { + yield arr.slice(offset, offset + sliceSize); + offset += sliceSize; } - }, - 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); - }, - byteArrayToString: function byteArrayToString(bytes) { return [String.fromCharCode(byte) for each (byte in bytes)].join(""); }, /** * Generate a string of random bytes. */ generateRandomBytes: function generateRandomBytes(length) { @@ -1158,26 +1116,33 @@ let Utils = { // See Bug 562431, comment 4. safeAtoB: function safeAtoB(b64) { let len = b64.length; let over = len % 4; return over ? atob(b64.substr(0, len - over)) : atob(b64); }, /** - * Create an array like the first but without elements of the second + * Create an array like the first but without elements of the second. Reuse + * arrays if possible. */ arraySub: function arraySub(minuend, subtrahend) { + if (!minuend.length || !subtrahend.length) + return minuend; return minuend.filter(function(i) subtrahend.indexOf(i) == -1); }, /** - * Build the union of two arrays. + * Build the union of two arrays. Reuse arrays if possible. */ arrayUnion: function arrayUnion(foo, bar) { + if (!foo.length) + return bar; + if (!bar.length) + return foo; return foo.concat(Utils.arraySub(bar, foo)); }, bind2: function Async_bind2(object, method) { return function innerBind() { return method.apply(object, arguments); }; }, mpLocked: function mpLocked() { @@ -1206,84 +1171,27 @@ let Utils = { try { sdr.encryptString("bacon"); return true; } catch(e) {} 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) */ checkAppReady: function checkAppReady() { // Watch for app-quit notification to stop any sync calls Svc.Obs.add("quit-application", function() { Utils.checkAppReady = function() { throw Components.Exception("App. Quitting", Cr.NS_ERROR_ABORT); }; }); // 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; } }; XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() { let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; return converter; @@ -1299,22 +1207,31 @@ Svc.Obs = Observers; let _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ? "@mozilla.org/suite/sessionstore;1" : "@mozilla.org/browser/sessionstore;1"; [["Form", "@mozilla.org/satchel/form-history;1", "nsIFormHistory2"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["KeyFactory", "@mozilla.org/security/keyobjectfactory;1", "nsIKeyObjectFactory"], - ["Private", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"], ["Session", _sessionCID, "nsISessionStore"] ].forEach(function([name, contract, iface]) { XPCOMUtils.defineLazyServiceGetter(Svc, name, contract, iface); }); +// nsIPrivateBrowsingService is not implemented in mobile Firefox. +// Svc.Private should just return undefined in this case instead of throwing. +XPCOMUtils.defineLazyGetter(Svc, "Private", function() { + try { + return Cc["@mozilla.org/privatebrowsing;1"].getService(Ci["nsIPrivateBrowsingService"]); + } catch (e) { + return undefined; + } +}); + Svc.__defineGetter__("Crypto", function() { let cryptoSvc; let ns = {}; Cu.import("resource://services-crypto/WeaveCrypto.js", ns); cryptoSvc = new ns.WeaveCrypto(); delete Svc.Crypto; return Svc.Crypto = cryptoSvc; });
new file mode 100644 --- /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(); +}
new file mode 100644 --- /dev/null +++ b/services/sync/tests/unit/test_async_querySpinningly.js @@ -0,0 +1,85 @@ +_("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 + +function run_test() { + initTestLogging("Trace"); + + _("Using the form service to test queries"); + function c(query) Svc.Form.DBConnection.createStatement(query); + + _("Make sure the call is async and allows other events to process"); + let isAsync = false; + Utils.delay(function() isAsync = true, 0); + do_check_false(isAsync); + + _("Empty out the formhistory table"); + let r0 = Async.querySpinningly(c("DELETE FROM moz_formhistory")); + do_check_eq(r0, null); + + _("Make sure there's nothing there"); + let r1 = Async.querySpinningly(c("SELECT 1 FROM moz_formhistory")); + do_check_eq(r1, null); + + _("Insert a row"); + 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 = 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 = 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 = 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 = 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 = 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 = Async.querySpinningly(c("UPDATE moz_formhistory SET value = 'updated' WHERE fieldname = 'more'")); + do_check_eq(r8, null); + + _("Get the updated"); + 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 = 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 = Async.querySpinningly(query); + } catch(e) { + except = e; + } + do_check_true(!!except); + do_check_eq(except.result, SQLITE_CONSTRAINT_VIOLATION); + + _("Cleaning up"); + Async.querySpinningly(c("DELETE FROM moz_formhistory")); + + _("Make sure the timeout got to run before this function ends"); + do_check_true(isAsync); +}
--- a/services/sync/tests/unit/test_bookmark_engine.js +++ b/services/sync/tests/unit/test_bookmark_engine.js @@ -1,12 +1,13 @@ 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"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); Engines.register(BookmarksEngine); var syncTesting = new SyncTestingInfrastructure();
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js +++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js @@ -44,17 +44,17 @@ function clearBookmarks() { PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.tagsFolder); PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.toolbarFolder); PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.unfiledBookmarksFolder); startCount = smartBookmarkCount(); } // Verify that Places smart bookmarks have their annotation uploaded and // handled locally. -function test_annotation_uploaded() { +add_test(function test_annotation_uploaded() { let startCount = smartBookmarkCount(); _("Start count is " + startCount); if (startCount > 0) { // This can happen in XULRunner. clearBookmarks(); _("Start count is now " + startCount); @@ -163,23 +163,23 @@ function test_annotation_uploaded() { store.update(newRecord); do_check_eq("LeastVisited", PlacesUtils.annotations.getItemAnnotation( newID, SMART_BOOKMARKS_ANNO)); } finally { // Clean up. store.wipe(); - server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); + server.stop(run_next_test); } -} +}); -function test_smart_bookmarks_duped() { +add_test(function test_smart_bookmarks_duped() { let parent = PlacesUtils.toolbarFolderId; let uri = Utils.makeURI("place:redirectsMode=" + Ci.nsINavHistoryQueryOptions.REDIRECTS_MODE_TARGET + "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING + "&maxResults=10"); let title = "Most Visited"; @@ -230,19 +230,18 @@ function test_smart_bookmarks_duped() { } finally { // Clean up. store.wipe(); server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); } -} +}); function run_test() { initTestLogging("Trace"); Log4Moz.repository.getLogger("Engine.Bookmarks").level = Log4Moz.Level.Trace; generateNewKeys(); - test_annotation_uploaded(); - test_smart_bookmarks_duped(); + run_next_test(); }
--- a/services/sync/tests/unit/test_corrupt_keys.js +++ b/services/sync/tests/unit/test_corrupt_keys.js @@ -4,17 +4,17 @@ Cu.import("resource://services-sync/engi Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/status.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-sync/engines/tabs.js"); Cu.import("resource://services-sync/engines/history.js"); Cu.import("resource://services-sync/log4moz.js"); -function test_locally_changed_keys() { +add_test(function test_locally_changed_keys() { let passphrase = "abcdeabcdeabcdeabcdeabcdea"; // Tracking info/collections. let collectionsHelper = track_collections_helper(); let upd = collectionsHelper.with_updated_collection; let collections = collectionsHelper.collections; let keysWBO = new ServerWBO("keys"); @@ -28,17 +28,16 @@ function test_locally_changed_keys() { return function() { hmacErrorCount++; return f.call(this); }; } Weave.Service.handleHMACEvent = counting(Weave.Service.handleHMACEvent); - do_test_pending(); let server = httpd_setup({ // Special. "/1.1/johndoe/storage/meta/global": upd("meta", meta_global.handler()), "/1.1/johndoe/info/collections": collectionsHelper.handler, "/1.1/johndoe/storage/crypto/keys": upd("crypto", keysWBO.handler()), // Track modified times. "/1.1/johndoe/storage/clients": upd("clients", clients.handler()), @@ -222,23 +221,20 @@ function test_locally_changed_keys() { do_check_true(collections.crypto > old_key_time); do_check_eq(hmacErrorCount, 6); do_check_false(store.urlExists("http://foo/bar?record-no--5")); do_check_false(store.urlExists("http://foo/bar?record-no--6")); do_check_false(store.urlExists("http://foo/bar?record-no--7")); do_check_false(store.urlExists("http://foo/bar?record-no--8")); do_check_false(store.urlExists("http://foo/bar?record-no--9")); - // Clean up. - Weave.Service.startOver(); - } finally { Weave.Svc.Prefs.resetBranch(""); - server.stop(do_test_finished); + server.stop(run_next_test); } -} +}); function run_test() { let logger = Log4Moz.repository.rootLogger; Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - test_locally_changed_keys(); + run_next_test(); }
--- a/services/sync/tests/unit/test_history_engine.js +++ b/services/sync/tests/unit/test_history_engine.js @@ -2,17 +2,17 @@ Cu.import("resource://services-sync/reco Cu.import("resource://services-sync/engines/history.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/identity.js"); Cu.import("resource://services-sync/util.js"); var syncTesting = new SyncTestingInfrastructure(); -function test_processIncoming_mobile_history_batched() { +add_test(function test_processIncoming_mobile_history_batched() { _("SyncEngine._processIncoming works on history engine."); let FAKE_DOWNLOAD_LIMIT = 100; Svc.Prefs.set("clusterURL", "http://localhost:8080/"); Svc.Prefs.set("username", "foo"); Svc.Prefs.set("client.type", "mobile"); PlacesUtils.history.removeAllPages(); @@ -44,17 +44,16 @@ function test_processIncoming_mobile_his let wbo = new ServerWBO(id, payload); wbo.modified = modified; collection.wbos[id] = wbo; } let server = sync_httpd_setup({ "/1.1/foo/storage/history": collection.handler() }); - do_test_pending(); let engine = new HistoryEngine("history"); let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL)); meta_global.payload.engines = {history: {version: engine.version, syncID: engine.syncID}}; try { @@ -124,15 +123,15 @@ function test_processIncoming_mobile_his } } finally { PlacesUtils.history.removeAllPages(); server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); } -} +}); function run_test() { generateNewKeys(); - test_processIncoming_mobile_history_batched(); + run_next_test(); }
--- a/services/sync/tests/unit/test_history_store.js +++ b/services/sync/tests/unit/test_history_store.js @@ -1,10 +1,11 @@ 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; const TIMESTAMP2 = (Date.now() - 6592903) * 1000; const TIMESTAMP3 = (Date.now() - 123894) * 1000; function queryPlaces(uri, options) { let query = PlacesUtils.history.getNewQuery(); @@ -161,17 +162,17 @@ add_test(function test_null_title() { }); add_test(function test_invalid_records() { _("Make sure we handle invalid URLs in places databases gracefully."); let query = "INSERT INTO moz_places " + "(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."); let invalid_uri_guid = Utils.makeGUID(); let failed = store.applyIncomingBatch([{ id: invalid_uri_guid, histUri: ":::::::::::::::", title: "Doesn't have a valid URI",
--- a/services/sync/tests/unit/test_places_guid_downgrade.js +++ b/services/sync/tests/unit/test_places_guid_downgrade.js @@ -1,8 +1,9 @@ +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"); Cu.import("resource://services-sync/engines/bookmarks.js"); const kDBName = "places.sqlite"; const storageSvc = Cc["@mozilla.org/storage/service;1"] .getService(Ci.mozIStorageService); @@ -99,33 +100,33 @@ function test_history_guids() { dump("fxguid: " + fxguid + "\n"); dump("tbguid: " + tbguid + "\n"); _("History: Verify GUIDs are added to the guid column."); let stmt = PlacesUtils.history.DBConnection.createAsyncStatement( "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."); stmt = PlacesUtils.history.DBConnection.createAsyncStatement( "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); } function test_bookmark_guids() { let engine = new BookmarksEngine(); let store = engine._store; let fxid = PlacesUtils.bookmarks.insertBookmark( @@ -142,35 +143,35 @@ function test_bookmark_guids() { let fxguid = store.GUIDForId(fxid); let tbguid = store.GUIDForId(tbid); _("Bookmarks: Verify GUIDs are added to the guid column."); let stmt = PlacesUtils.history.DBConnection.createAsyncStatement( "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); _("Bookmarks: Verify GUIDs weren't added to annotations."); stmt = PlacesUtils.history.DBConnection.createAsyncStatement( "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); } function run_test() { setPlacesDatabase("places_v10_from_v11.sqlite"); _("Verify initial setup: v11 database is available"); test_initial_state();
--- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -296,24 +296,23 @@ function run_test() { _("JSON conversion of response body"); let res7 = new Resource("http://localhost:8080/json"); content = res7.get(); do_check_eq(content, JSON.stringify(sample_data)); do_check_eq(content.status, 200); do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data)); - _("X-Weave-Timestamp header updates Resource.serverTime"); + _("X-Weave-Timestamp header updates AsyncResource.serverTime"); // Before having received any response containing the - // X-Weave-Timestamp header, Resource.serverTime is null. - do_check_eq(Resource.serverTime, null); + // X-Weave-Timestamp header, AsyncResource.serverTime is null. + do_check_eq(AsyncResource.serverTime, null); let res8 = new Resource("http://localhost:8080/timestamp"); content = res8.get(); - do_check_eq(Resource.serverTime, TIMESTAMP); - + do_check_eq(AsyncResource.serverTime, TIMESTAMP); _("GET: no special request headers"); let res9 = new Resource("http://localhost:8080/headers"); content = res9.get(); do_check_eq(content, '{}'); _("PUT: Content-Type defaults to text/plain"); content = res9.put('data'); @@ -480,10 +479,23 @@ function run_test() { error = undefined; try { content = res19.get(); } catch (ex) { error = ex; } do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT); + _("Testing URI construction."); + let args = []; + args.push("newer=" + 1234); + args.push("limit=" + 1234); + args.push("sort=" + 1234); + + let query = "?" + args.join("&"); + + let uri1 = Utils.makeURL("http://foo/" + query); + let uri2 = Utils.makeURL("http://foo/"); + uri2.query = query; + do_check_eq(uri1.query, uri2.query); + server.stop(do_test_finished); }
--- a/services/sync/tests/unit/test_resource_async.js +++ b/services/sync/tests/unit/test_resource_async.js @@ -408,25 +408,25 @@ function run_test() { do_check_eq(content.status, 200); do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data)); do_test_finished(); next(); })); }, function (next) { - _("X-Weave-Timestamp header updates Resource.serverTime"); + _("X-Weave-Timestamp header updates AsyncResource.serverTime"); do_test_pending(); // Before having received any response containing the - // X-Weave-Timestamp header, Resource.serverTime is null. - do_check_eq(Resource.serverTime, null); + // X-Weave-Timestamp header, AsyncResource.serverTime is null. + do_check_eq(AsyncResource.serverTime, null); let res8 = new AsyncResource("http://localhost:8080/timestamp"); res8.get(ensureThrows(function (error, content) { do_check_eq(error, null); - do_check_eq(Resource.serverTime, TIMESTAMP); + do_check_eq(AsyncResource.serverTime, TIMESTAMP); do_test_finished(); next(); })); }, function (next) { _("GET: no special request headers"); do_test_pending(); @@ -680,14 +680,30 @@ function run_test() { res19.ABORT_TIMEOUT = 0; res19.get(ensureThrows(function (error, content) { do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT); next(); })); }, function (next) { + _("Testing URI construction."); + let args = []; + args.push("newer=" + 1234); + args.push("limit=" + 1234); + args.push("sort=" + 1234); + + let query = "?" + args.join("&"); + + let uri1 = Utils.makeURL("http://foo/" + query); + let uri2 = Utils.makeURL("http://foo/"); + uri2.query = query; + do_check_eq(uri1.query, uri2.query); + next(); + + }, function (next) { + // Don't quit test harness before server shuts down. server.stop(do_test_finished); })(); }
--- a/services/sync/tests/unit/test_syncengine.js +++ b/services/sync/tests/unit/test_syncengine.js @@ -94,34 +94,66 @@ function test_toFetch() { do_check_eq(engine.toFetch.length, 2); do_check_eq(engine.toFetch[0], toFetch[0]); do_check_eq(engine.toFetch[1], toFetch[1]); } finally { syncTesting = new SyncTestingInfrastructure(makeSteamEngine); } } +function test_previousFailed() { + _("SyncEngine.previousFailed corresponds to file on disk"); + let syncTesting = new SyncTestingInfrastructure(); + const filename = "weave/failed/steam.json"; + let engine = makeSteamEngine(); + try { + // Ensure pristine environment + do_check_eq(engine.previousFailed.length, 0); + + // Write file to disk + let previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; + engine.previousFailed = previousFailed; + do_check_eq(engine.previousFailed, previousFailed); + // previousFailed is written asynchronously + engine._store._sleep(0); + let fakefile = syncTesting.fakeFilesystem.fakeContents[filename]; + do_check_eq(fakefile, JSON.stringify(previousFailed)); + + // Read file from disk + previousFailed = [Utils.makeGUID(), Utils.makeGUID()]; + syncTesting.fakeFilesystem.fakeContents[filename] = JSON.stringify(previousFailed); + engine.loadPreviousFailed(); + do_check_eq(engine.previousFailed.length, 2); + do_check_eq(engine.previousFailed[0], previousFailed[0]); + do_check_eq(engine.previousFailed[1], previousFailed[1]); + } finally { + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + function test_resetClient() { _("SyncEngine.resetClient resets lastSync and toFetch"); let syncTesting = new SyncTestingInfrastructure(); let engine = makeSteamEngine(); try { // Ensure pristine environment do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined); do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), undefined); do_check_eq(engine.toFetch.length, 0); engine.lastSync = 123.45; engine.lastSyncLocal = 67890; engine.toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; + engine.previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; engine.resetClient(); do_check_eq(engine.lastSync, 0); do_check_eq(engine.lastSyncLocal, 0); do_check_eq(engine.toFetch.length, 0); + do_check_eq(engine.previousFailed.length, 0); } finally { syncTesting = new SyncTestingInfrastructure(makeSteamEngine); Svc.Prefs.resetBranch(""); } } function test_wipeServer() { _("SyncEngine.wipeServer deletes server data and resets the client."); @@ -154,11 +186,12 @@ function test_wipeServer() { } } function run_test() { test_url_attributes(); test_syncID(); test_lastSync(); test_toFetch(); + test_previousFailed(); test_resetClient(); test_wipeServer(); }
--- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -593,17 +593,17 @@ function test_processIncoming_store_toFe server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); } } function test_processIncoming_resume_toFetch() { - _("toFetch items left over from previous syncs are fetched on the next sync, along with new items."); + _("toFetch and previousFailed items left over from previous syncs are fetched on the next sync, along with new items."); let syncTesting = new SyncTestingInfrastructure(); Svc.Prefs.set("clusterURL", "http://localhost:8080/"); Svc.Prefs.set("username", "foo"); const LASTSYNC = Date.now() / 1000; // Server records that will be downloaded let collection = new ServerCollection(); @@ -611,25 +611,33 @@ function test_processIncoming_resume_toF 'flying', encryptPayload({id: 'flying', denomination: "LNER Class A3 4472"})); collection.wbos.scotsman = new ServerWBO( 'scotsman', encryptPayload({id: 'scotsman', denomination: "Flying Scotsman"})); collection.wbos.rekolok = new ServerWBO( 'rekolok', encryptPayload({id: 'rekolok', denomination: "Rekonstruktionslokomotive"})); + for (var i = 0; i < 3; i++) { + let id = 'failed' + i; + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); + let wbo = new ServerWBO(id, payload); + wbo.modified = LASTSYNC - 10; + collection.wbos[id] = wbo; + } collection.wbos.flying.modified = collection.wbos.scotsman.modified = LASTSYNC - 10; collection.wbos.rekolok.modified = LASTSYNC + 10; // Time travel 10 seconds into the future but still download the above WBOs. let engine = makeSteamEngine(); engine.lastSync = LASTSYNC; engine.toFetch = ["flying", "scotsman"]; + engine.previousFailed = ["failed0", "failed1", "failed2"]; let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL)); meta_global.payload.engines = {steam: {version: engine.version, syncID: engine.syncID}}; let server = sync_httpd_setup({ "/1.1/foo/storage/steam": collection.handler() }); do_test_pending(); @@ -643,17 +651,20 @@ function test_processIncoming_resume_toF engine._syncStartup(); engine._processIncoming(); // Local records have been created from the server data. do_check_eq(engine._store.items.flying, "LNER Class A3 4472"); do_check_eq(engine._store.items.scotsman, "Flying Scotsman"); do_check_eq(engine._store.items.rekolok, "Rekonstruktionslokomotive"); - + do_check_eq(engine._store.items.failed0, "Record No. 0"); + do_check_eq(engine._store.items.failed1, "Record No. 1"); + do_check_eq(engine._store.items.failed2, "Record No. 2"); + do_check_eq(engine.previousFailed.length, 0); } finally { server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); } } @@ -694,22 +705,23 @@ function test_processIncoming_applyIncom try { // Confirm initial environment do_check_eq([id for (id in engine._store.items)].length, 0); engine._syncStartup(); engine._processIncoming(); - // Records have been applied. + // Records have been applied and the expected failures have failed. do_check_eq([id for (id in engine._store.items)].length, APPLY_BATCH_SIZE - 1 - 2); - do_check_eq(engine.toFetch.length, 2); - do_check_eq(engine.toFetch[0], "record-no-0"); - do_check_eq(engine.toFetch[1], "record-no-8"); + do_check_eq(engine.toFetch.length, 0); + do_check_eq(engine.previousFailed.length, 2); + do_check_eq(engine.previousFailed[0], "record-no-0"); + do_check_eq(engine.previousFailed[1], "record-no-8"); } finally { server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); } } @@ -765,16 +777,192 @@ function test_processIncoming_applyIncom } finally { server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); } } +function test_processIncoming_failed_items_reported_once() { + _("Ensure that failed records are reported only once."); + let syncTesting = new SyncTestingInfrastructure(); + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + Svc.Prefs.set("username", "foo"); + + const APPLY_BATCH_SIZE = 5; + const NUMBER_OF_RECORDS = 15; + + // Engine that fails the first record. + let engine = makeSteamEngine(); + engine.applyIncomingBatchSize = APPLY_BATCH_SIZE; + engine._store._applyIncomingBatch = engine._store.applyIncomingBatch; + engine._store.applyIncomingBatch = function (records) { + engine._store._applyIncomingBatch(records.slice(1)); + return [records[0].id]; + }; + + // Create a batch of server side records. + let collection = new ServerCollection(); + for (var i = 0; i < NUMBER_OF_RECORDS; i++) { + let id = 'record-no-' + i; + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); + collection.wbos[id] = new ServerWBO(id, payload); + } + + let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL)); + meta_global.payload.engines = {steam: {version: engine.version, + syncID: engine.syncID}}; + let server = sync_httpd_setup({ + "/1.1/foo/storage/steam": collection.handler() + }); + do_test_pending(); + + try { + let called = 0; + let counts; + + // Confirm initial environment. + do_check_eq(engine.lastSync, 0); + do_check_eq(engine.toFetch.length, 0); + do_check_eq(engine.previousFailed.length, 0); + do_check_eq([id for (id in engine._store.items)].length, 0); + + Svc.Obs.add("weave:engine:sync:apply-failed", function(count) { + _("Called with " + JSON.stringify(counts)); + counts = count; + called++; + }); + + // Do sync. + engine._syncStartup(); + engine._processIncoming(); + + // Confirm failures. + do_check_eq([id for (id in engine._store.items)].length, 12); + do_check_eq(engine.previousFailed.length, 3); + do_check_eq(engine.previousFailed[0], "record-no-0"); + do_check_eq(engine.previousFailed[1], "record-no-5"); + do_check_eq(engine.previousFailed[2], "record-no-10"); + + // There are newly failed records and they are reported. + do_check_eq(called, 1); + do_check_eq(counts.failed, 3); + do_check_eq(counts.applied, 15); + do_check_eq(counts.newFailed, 3); + + // Sync again, 1 of the failed items are the same, the rest didn't fail. + engine._processIncoming(); + + // Confirming removed failures. + do_check_eq([id for (id in engine._store.items)].length, 14); + do_check_eq(engine.previousFailed.length, 1); + do_check_eq(engine.previousFailed[0], "record-no-0"); + + // Failures weren't notified again because there were no newly failed items. + do_check_eq(called, 1); + do_check_eq(counts.failed, 3); + do_check_eq(counts.applied, 15); + do_check_eq(counts.newFailed, 3); + } finally { + server.stop(do_test_finished); + Svc.Prefs.resetBranch(""); + Records.clearCache(); + } +} + + +function test_processIncoming_previousFailed() { + _("Ensure that failed records are retried."); + let syncTesting = new SyncTestingInfrastructure(); + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + Svc.Prefs.set("username", "foo"); + Svc.Prefs.set("client.type", "mobile"); + + const APPLY_BATCH_SIZE = 4; + const NUMBER_OF_RECORDS = 14; + + // Engine that fails the first 2 records. + let engine = makeSteamEngine(); + engine.mobileGUIDFetchBatchSize = engine.applyIncomingBatchSize = APPLY_BATCH_SIZE; + engine._store._applyIncomingBatch = engine._store.applyIncomingBatch; + engine._store.applyIncomingBatch = function (records) { + engine._store._applyIncomingBatch(records.slice(2)); + return [records[0].id, records[1].id]; + }; + + // Create a batch of server side records. + let collection = new ServerCollection(); + for (var i = 0; i < NUMBER_OF_RECORDS; i++) { + let id = 'record-no-' + i; + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); + collection.wbos[id] = new ServerWBO(id, payload); + } + + let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL)); + meta_global.payload.engines = {steam: {version: engine.version, + syncID: engine.syncID}}; + let server = sync_httpd_setup({ + "/1.1/foo/storage/steam": collection.handler() + }); + do_test_pending(); + + try { + // Confirm initial environment. + do_check_eq(engine.lastSync, 0); + do_check_eq(engine.toFetch.length, 0); + do_check_eq(engine.previousFailed.length, 0); + do_check_eq([id for (id in engine._store.items)].length, 0); + + // Initial failed items in previousFailed to be reset. + let previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; + engine.previousFailed = previousFailed; + do_check_eq(engine.previousFailed, previousFailed); + + // Do sync. + engine._syncStartup(); + engine._processIncoming(); + + // Expected result: 4 sync batches with 2 failures each => 8 failures + do_check_eq([id for (id in engine._store.items)].length, 6); + do_check_eq(engine.previousFailed.length, 8); + do_check_eq(engine.previousFailed[0], "record-no-0"); + do_check_eq(engine.previousFailed[1], "record-no-1"); + do_check_eq(engine.previousFailed[2], "record-no-4"); + do_check_eq(engine.previousFailed[3], "record-no-5"); + do_check_eq(engine.previousFailed[4], "record-no-8"); + do_check_eq(engine.previousFailed[5], "record-no-9"); + do_check_eq(engine.previousFailed[6], "record-no-12"); + do_check_eq(engine.previousFailed[7], "record-no-13"); + + // Sync again with the same failed items (records 0, 1, 8, 9). + engine._processIncoming(); + + // A second sync with the same failed items should not add the same items again. + // Items that did not fail a second time should no longer be in previousFailed. + do_check_eq([id for (id in engine._store.items)].length, 10); + do_check_eq(engine.previousFailed.length, 4); + do_check_eq(engine.previousFailed[0], "record-no-0"); + do_check_eq(engine.previousFailed[1], "record-no-1"); + do_check_eq(engine.previousFailed[2], "record-no-8"); + do_check_eq(engine.previousFailed[3], "record-no-9"); + + // Refetched items that didn't fail the second time are in engine._store.items. + do_check_eq(engine._store.items['record-no-4'], "Record No. 4"); + do_check_eq(engine._store.items['record-no-5'], "Record No. 5"); + do_check_eq(engine._store.items['record-no-12'], "Record No. 12"); + do_check_eq(engine._store.items['record-no-13'], "Record No. 13"); + } finally { + server.stop(do_test_finished); + Svc.Prefs.resetBranch(""); + Records.clearCache(); + } +} + + function test_processIncoming_failed_records() { _("Ensure that failed records from _reconcile and applyIncomingBatch are refetched."); let syncTesting = new SyncTestingInfrastructure(); Svc.Prefs.set("clusterURL", "http://localhost:8080/"); Svc.Prefs.set("username", "foo"); // Let's create three and a bit batches worth of server side records. let collection = new ServerCollection(); @@ -836,16 +1024,17 @@ function test_processIncoming_failed_rec }); do_test_pending(); try { // Confirm initial environment do_check_eq(engine.lastSync, 0); do_check_eq(engine.toFetch.length, 0); + do_check_eq(engine.previousFailed.length, 0); do_check_eq([id for (id in engine._store.items)].length, 0); let observerSubject; let observerData; Svc.Obs.add("weave:engine:sync:apply-failed", function onApplyFailed(subject, data) { Svc.Obs.remove("weave:engine:sync:apply-failed", onApplyFailed); observerSubject = subject; @@ -855,21 +1044,21 @@ function test_processIncoming_failed_rec engine._syncStartup(); engine._processIncoming(); // Ensure that all records but the bogus 4 have been applied. do_check_eq([id for (id in engine._store.items)].length, NUMBER_OF_RECORDS - BOGUS_RECORDS.length); // Ensure that the bogus records will be fetched again on the next sync. - do_check_eq(engine.toFetch.length, BOGUS_RECORDS.length); - engine.toFetch.sort(); + do_check_eq(engine.previousFailed.length, BOGUS_RECORDS.length); + engine.previousFailed.sort(); BOGUS_RECORDS.sort(); - for (let i = 0; i < engine.toFetch.length; i++) { - do_check_eq(engine.toFetch[i], BOGUS_RECORDS[i]); + for (let i = 0; i < engine.previousFailed.length; i++) { + do_check_eq(engine.previousFailed[i], BOGUS_RECORDS[i]); } // Ensure the observer was notified do_check_eq(observerData, engine.name); do_check_eq(observerSubject.failed, BOGUS_RECORDS.length); // Testing batching of failed item fetches. // Try to sync again. Ensure that we split the request into chunks to avoid @@ -947,34 +1136,35 @@ function test_processIncoming_decrypt_fa "/1.1/foo/storage/steam": collection.handler() }); do_test_pending(); try { // Confirm initial state do_check_eq(engine.toFetch.length, 0); + do_check_eq(engine.previousFailed.length, 0); let observerSubject; let observerData; Svc.Obs.add("weave:engine:sync:apply-failed", function onApplyFailed(subject, data) { Svc.Obs.remove("weave:engine:sync:apply-failed", onApplyFailed); observerSubject = subject; observerData = data; }); engine.lastSync = collection.wbos.nojson.modified - 1; engine.sync(); - do_check_eq(engine.toFetch.length, 4); - do_check_eq(engine.toFetch[0], "nojson"); - do_check_eq(engine.toFetch[1], "nojson2"); - do_check_eq(engine.toFetch[2], "nodecrypt"); - do_check_eq(engine.toFetch[3], "nodecrypt2"); + do_check_eq(engine.previousFailed.length, 4); + do_check_eq(engine.previousFailed[0], "nojson"); + do_check_eq(engine.previousFailed[1], "nojson2"); + do_check_eq(engine.previousFailed[2], "nodecrypt"); + do_check_eq(engine.previousFailed[3], "nodecrypt2"); // Ensure the observer was notified do_check_eq(observerData, engine.name); do_check_eq(observerSubject.applied, 2); do_check_eq(observerSubject.failed, 4); } finally { server.stop(do_test_finished); @@ -1452,16 +1642,18 @@ function run_test() { test_processIncoming_emptyServer(); test_processIncoming_createFromServer(); test_processIncoming_reconcile(); test_processIncoming_mobile_batchSize(); test_processIncoming_store_toFetch(); test_processIncoming_resume_toFetch(); test_processIncoming_applyIncomingBatchSize_smaller(); test_processIncoming_applyIncomingBatchSize_multiple(); + test_processIncoming_failed_items_reported_once(); + test_processIncoming_previousFailed(); test_processIncoming_failed_records(); test_processIncoming_decrypt_failed(); test_uploadOutgoing_toEmptyServer(); test_uploadOutgoing_failed(); test_uploadOutgoing_MAX_UPLOAD_RECORDS(); test_syncFinish_noDelete(); test_syncFinish_deleteByIds(); test_syncFinish_deleteLotsInBatches();
deleted file mode 100644 --- a/services/sync/tests/unit/test_utils_queryAsync.js +++ /dev/null @@ -1,85 +0,0 @@ -_("Make sure queryAsync will synchronously fetch rows for a query asyncly"); -Cu.import("resource://services-sync/util.js"); - -const SQLITE_CONSTRAINT_VIOLATION = 19; // http://www.sqlite.org/c3ref/c_abort.html - -function run_test() { - initTestLogging("Trace"); - - _("Using the form service to test queries"); - function c(query) Svc.Form.DBConnection.createStatement(query); - - _("Make sure the call is async and allows other events to process"); - let isAsync = false; - Utils.delay(function() isAsync = true, 0); - do_check_false(isAsync); - - _("Empty out the formhistory table"); - let r0 = Utils.queryAsync(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")); - do_check_eq(r1, null); - - _("Insert a row"); - let r2 = Utils.queryAsync(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"]); - 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"]); - 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"]); - 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'")); - do_check_eq(r6, null); - - _("Get multiple rows"); - let r7 = Utils.queryAsync(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'")); - do_check_eq(r8, null); - - _("Get the updated"); - let r9 = Utils.queryAsync(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"]); - 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); - } catch(e) { - except = e; - } - do_check_true(!!except); - do_check_eq(except.result, SQLITE_CONSTRAINT_VIOLATION); - - _("Cleaning up"); - Utils.queryAsync(c("DELETE FROM moz_formhistory")); - - _("Make sure the timeout got to run before this function ends"); - do_check_true(isAsync); -}
new file mode 100644 --- /dev/null +++ b/services/sync/tests/unit/test_utils_slices.js @@ -0,0 +1,48 @@ +Cu.import("resource://services-sync/util.js"); + +function run_test() { + let input = [1, 2, 3, 4, 5]; + + let err; + try { + Utils.slices(input, 0).next(); + } catch (ex) { + err = ex; + } + do_check_eq("Invalid slice size.", err); + + err = undefined; + try { + Utils.slices(input).next(); + } catch (ex) { + err = ex; + } + do_check_eq("Invalid slice size.", err); + + function slurp(g) [x for each (x in g)] + + let sliced1 = slurp(Utils.slices(input, 1)); + let sliced2 = slurp(Utils.slices(input, 2)); + let sliced3 = slurp(Utils.slices(input, 5)); + let sliced4 = slurp(Utils.slices(input, 7)); + + do_check_eq(sliced1.length, 5); + do_check_eq(sliced2.length, 3); + do_check_eq(sliced3.length, 1); + do_check_eq(sliced4.length, 1); + sliced1.every(function(x) x.length == 1); + _(JSON.stringify(sliced2)); + do_check_eq(sliced2[0].length, 2); + do_check_eq(sliced2[1].length, 2); + do_check_eq(sliced2[2].length, 1); + sliced3.every(function(x) x.length == 5); + sliced4.every(function(x) x.length == 5); + + let sliced5 = slurp(Utils.slices(["foo"], 50)); + do_check_eq(sliced5.length, 1); + do_check_eq(sliced5[0], "foo"); + + let sliced6 = slurp(Utils.slices([], 50)); + do_check_eq(sliced6.length, 1); + do_check_eq(sliced6[0].length, 0); +}
--- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -1,14 +1,16 @@ [DEFAULT] head = head_appinfo.js head_helpers.js head_http_server.js 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] [test_bookmark_legacy_microsummaries_support.js] [test_bookmark_livemarks.js] [test_bookmark_order.js] [test_bookmark_places_query_rewriting.js] [test_bookmark_record.js] @@ -88,14 +90,14 @@ tail = [test_utils_json.js] [test_utils_lazyStrings.js] [test_utils_lock.js] [test_utils_makeGUID.js] [test_utils_makeURI.js] [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] +[test_utils_slices.js] [test_utils_stackTrace.js] [test_utils_utf8.js]