Bug 612381 - Merge fx-sync to mozilla-central. a=blocking-beta8

This commit is contained in:
Philipp von Weitershausen 2010-11-15 14:01:58 -08:00
Родитель 488481b966 a2b8bef32a
Коммит 6633fdedf0
16 изменённых файлов: 1310 добавлений и 296 удалений

Просмотреть файл

@ -62,6 +62,10 @@ MULTI_DESKTOP_SYNC: 60 * 60 * 1000, // 1 hour
MULTI_MOBILE_SYNC: 5 * 60 * 1000, // 5 minutes
PARTIAL_DATA_SYNC: 60 * 1000, // 1 minute
// 50 is hardcoded here because of URL length restrictions.
// (GUIDs can be up to 64 chars long)
MOBILE_BATCH_SIZE: 50,
// score thresholds for early syncs
SINGLE_USER_THRESHOLD: 1000,
MULTI_DESKTOP_THRESHOLD: 500,

Просмотреть файл

@ -20,6 +20,7 @@
* Contributor(s):
* Dan Mills <thunder@mozilla.com>
* Myk Melez <myk@mozilla.org>
* Philipp von Weitershausen <philipp@weitershausen.de>
*
* 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
@ -282,7 +283,6 @@ Engine.prototype = {
function SyncEngine(name) {
Engine.call(this, name || "SyncEngine");
this.loadToFetch();
}
SyncEngine.prototype = {
__proto__: Engine.prototype,
@ -307,6 +307,9 @@ SyncEngine.prototype = {
Svc.Prefs.set(this.name + ".syncID", value);
},
/*
* lastSync is a timestamp in server time.
*/
get lastSync() {
return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0"));
},
@ -320,19 +323,27 @@ 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) {
this._toFetch = val;
Utils.jsonSave("toFetch/" + this.name, this, val);
/*
* 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.
Svc.Prefs.set(this.name + ".lastSyncLocal", value.toString());
},
loadToFetch: function loadToFetch() {
// Initialize to empty if there's no file
this._toFetch = [];
Utils.jsonLoad("toFetch/" + this.name, this, Utils.bind2(this, function(o)
this._toFetch = o));
/*
* Returns a mapping of IDs -> changed timestamp. Engine implementations
* can override this method to bypass the tracker for certain or all
* changed items.
*/
getChangedIDs: function getChangedIDs() {
return this._tracker.changedIDs;
},
// Create a new record using the store and add in crypto fields
@ -425,37 +436,41 @@ SyncEngine.prototype = {
CryptoMetas.set(meta.uri, meta);
}
// Mark all items to be uploaded, but treat them as changed from long ago
if (!this.lastSync) {
this._maybeLastSyncLocal = Date.now();
if (this.lastSync) {
this._modified = this.getChangedIDs();
// Clear the tracker now but remember the changed IDs in case we
// need to roll back.
this._backupChangedIDs = this._tracker.changedIDs;
this._tracker.clearChangedIDs();
} else {
// Mark all items to be uploaded, but treat them as changed from long ago
this._log.debug("First sync, uploading all items");
this._modified = {};
for (let id in this._store.getAllIDs())
this._tracker.addChangedID(id, 0);
this._modified[id] = 0;
}
let outnum = [i for (i in this._tracker.changedIDs)].length;
let outnum = [i for (i in this._modified)].length;
this._log.info(outnum + " outgoing items pre-reconciliation");
// Keep track of what to delete at the end of sync
this._delete = {};
},
// Generate outgoing records
// Process incoming records
_processIncoming: function SyncEngine__processIncoming() {
this._log.trace("Downloading & applying server changes");
// Figure out how many total items to fetch this sync; do less on mobile.
// 50 is hardcoded here because of URL length restrictions.
// (GUIDs can be up to 64 chars long)
let fetchNum = Infinity;
let batchSize = Infinity;
let newitems = new Collection(this.engineURL, this._recordObj);
if (Svc.Prefs.get("client.type") == "mobile") {
fetchNum = 50;
newitems.sort = "index";
batchSize = MOBILE_BATCH_SIZE;
}
newitems.newer = this.lastSync;
newitems.full = true;
newitems.limit = fetchNum;
newitems.limit = batchSize;
let count = {applied: 0, reconciled: 0};
let handled = [];
@ -489,7 +504,7 @@ SyncEngine.prototype = {
// Upload a new record to replace the bad one if we have it
if (this._store.itemExists(item.id))
this._tracker.addChangedID(item.id, 0);
this._modified[item.id] = 0;
}
this._tracker.ignoreAll = false;
Sync.sleep(0);
@ -502,16 +517,13 @@ SyncEngine.prototype = {
resp.failureCode = ENGINE_DOWNLOAD_FAIL;
throw resp;
}
// Subtract out the number of items we just got
fetchNum -= handled.length;
}
// Check if we got the maximum that we requested; get the rest if so
// Mobile: check if we got the maximum that we requested; get the rest if so.
let toFetch = [];
if (handled.length == newitems.limit) {
let guidColl = new Collection(this.engineURL);
guidColl.newer = this.lastSync;
guidColl.sort = "index";
let guids = guidColl.get();
if (!guids.success)
@ -521,20 +533,18 @@ SyncEngine.prototype = {
// were already waiting and prepend the new ones
let extra = Utils.arraySub(guids.obj, handled);
if (extra.length > 0)
this.toFetch = extra.concat(Utils.arraySub(this.toFetch, extra));
toFetch = extra.concat(Utils.arraySub(toFetch, extra));
}
// Process any backlog of GUIDs if we haven't fetched too many this sync
while (this.toFetch.length > 0 && fetchNum > 0) {
// Mobile: process any backlog of GUIDs
while (toFetch.length) {
// Reuse the original query, but get rid of the restricting params
newitems.limit = 0;
newitems.newer = 0;
// Get the first bunch of records and save the rest for later
let minFetch = Math.min(150, this.toFetch.length, fetchNum);
newitems.ids = this.toFetch.slice(0, minFetch);
this.toFetch = this.toFetch.slice(minFetch);
fetchNum -= minFetch;
newitems.ids = toFetch.slice(0, batchSize);
toFetch = toFetch.slice(batchSize);
// Reuse the existing record handler set earlier
let resp = newitems.get();
@ -548,7 +558,7 @@ SyncEngine.prototype = {
this.lastSync = this.lastModified;
this._log.info(["Records:", count.applied, "applied,", count.reconciled,
"reconciled,", this.toFetch.length, "left to fetch"].join(" "));
"reconciled."].join(" "));
},
/**
@ -609,7 +619,7 @@ SyncEngine.prototype = {
this._log.trace("Incoming: " + item);
this._log.trace("Reconcile step 1: Check for conflicts");
if (item.id in this._tracker.changedIDs) {
if (item.id in this._modified) {
// If the incoming and local changes are the same, skip
if (this._isEqual(item)) {
this._tracker.removeChangedID(item.id);
@ -618,7 +628,7 @@ SyncEngine.prototype = {
// Records differ so figure out which to take
let recordAge = Resource.serverTime - item.modified;
let localAge = Date.now() / 1000 - this._tracker.changedIDs[item.id];
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)
@ -645,7 +655,7 @@ SyncEngine.prototype = {
// Upload outgoing records
_uploadOutgoing: function SyncEngine__uploadOutgoing() {
let failed = {};
let outnum = [i for (i in this._tracker.changedIDs)].length;
let outnum = [i for (i in this._modified)].length;
if (outnum) {
this._log.trace("Preparing " + outnum + " outgoing records");
@ -663,7 +673,7 @@ SyncEngine.prototype = {
throw resp;
}
// Record the modified time of the upload
// Update server timestamp from the upload.
let modified = resp.headers["x-weave-timestamp"];
if (modified > this.lastSync)
this.lastSync = modified;
@ -672,7 +682,7 @@ SyncEngine.prototype = {
// can mark them changed again.
let failed_ids = [];
for (let id in resp.obj.failed) {
failed[id] = this._tracker.changedIDs[id];
failed[id] = this._modified[id];
failed_ids.push(id);
}
if (failed_ids.length)
@ -683,7 +693,7 @@ SyncEngine.prototype = {
up.clearRecords();
});
for (let id in this._tracker.changedIDs) {
for (let id in this._modified) {
try {
let out = this._createRecord(id);
if (this._log.level <= Log4Moz.Level.Trace)
@ -707,7 +717,11 @@ SyncEngine.prototype = {
if (count % MAX_UPLOAD_RECORDS > 0)
doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all");
}
this._tracker.clearChangedIDs();
// Update local timestamp.
this.lastSyncLocal = this._maybeLastSyncLocal;
delete this._modified;
delete this._backupChangedIDs;
// Mark failed WBOs as changed again so they are reuploaded next time.
for (let id in failed) {
@ -744,6 +758,15 @@ SyncEngine.prototype = {
}
},
_rollback: function _rollback() {
if (!this._backupChangedIDs)
return;
for (let [id, when] in Iterator(this._backupChangedIDs)) {
this._tracker.addChangedID(id, when);
}
},
_sync: function SyncEngine__sync() {
try {
this._syncStartup();
@ -754,6 +777,7 @@ SyncEngine.prototype = {
this._syncFinish();
}
catch (e) {
this._rollback();
this._log.warn("Sync failed");
throw e;
}
@ -788,7 +812,6 @@ SyncEngine.prototype = {
_resetClient: function SyncEngine__resetClient() {
this.resetLastSync();
this.toFetch = [];
},
wipeServer: function wipeServer(ignoreCrypto) {

Просмотреть файл

@ -45,6 +45,7 @@ const Cu = Components.utils;
const PARENT_ANNO = "weave/parent";
const PREDECESSOR_ANNO = "weave/predecessor";
const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
const FOLDER_SORTINDEX = 1000000;
try {
Cu.import("resource://gre/modules/PlacesUtils.jsm");
@ -862,6 +863,10 @@ BookmarksStore.prototype = {
},
_calculateIndex: function _calculateIndex(record) {
// Ensure folders have a very high sort index so they're not synced last.
if (record.type == "folder")
return FOLDER_SORTINDEX;
// For anything directly under the toolbar, give it a boost of more than an
// unvisited bookmark
let index = 0;

Просмотреть файл

@ -50,31 +50,6 @@ Cu.import("resource://services-sync/type_records/history.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/log4moz.js");
// Create some helper functions to handle GUIDs
function setGUID(uri, guid) {
if (arguments.length == 1)
guid = Utils.makeGUID();
try {
Utils.anno(uri, GUID_ANNO, guid, "WITH_HISTORY");
} catch (ex) {
let log = Log4Moz.repository.getLogger("Engine.History");
log.warn("Couldn't annotate URI " + uri + ": " + ex);
}
return guid;
}
function GUIDForUri(uri, create) {
try {
// Use the existing GUID if it exists
return Utils.anno(uri, GUID_ANNO);
}
catch (ex) {
// Give the uri a GUID if it doesn't have one
if (create)
return setGUID(uri);
}
}
function HistoryEngine() {
SyncEngine.call(this, "History");
}
@ -87,7 +62,7 @@ HistoryEngine.prototype = {
_sync: Utils.batchSync("History", SyncEngine),
_findDupe: function _findDupe(item) {
return GUIDForUri(item.histUri);
return this._store.GUIDForUri(item.histUri);
}
};
@ -142,6 +117,127 @@ HistoryStore.prototype = {
return this.__haveTempTables;
},
get _addGUIDAnnotationNameStm() {
let stmt = this._getStmt(
"INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)");
stmt.params.anno_name = GUID_ANNO;
return stmt;
},
get _checkGUIDPageAnnotationStm() {
let base =
"SELECT h.id AS place_id, " +
"(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS name_id, " +
"a.id AS anno_id, a.dateAdded AS anno_date ";
let stmt;
if (this._haveTempTables) {
// Gecko <2.0
stmt = this._getStmt(base +
"FROM (SELECT id FROM moz_places_temp WHERE url = :page_url " +
"UNION " +
"SELECT id FROM moz_places WHERE url = :page_url) AS h " +
"LEFT JOIN moz_annos a ON a.place_id = h.id " +
"AND a.anno_attribute_id = name_id");
} else {
// Gecko 2.0
stmt = this._getStmt(base +
"FROM moz_places h " +
"LEFT JOIN moz_annos a ON a.place_id = h.id " +
"AND a.anno_attribute_id = name_id " +
"WHERE h.url = :page_url");
}
stmt.params.anno_name = GUID_ANNO;
return stmt;
},
get _addPageAnnotationStm() {
return this._getStmt(
"INSERT OR REPLACE INTO moz_annos " +
"(id, place_id, anno_attribute_id, mime_type, content, flags, " +
"expiration, type, dateAdded, lastModified) " +
"VALUES (:id, :place_id, :name_id, :mime_type, :content, :flags, " +
":expiration, :type, :date_added, :last_modified)");
},
// Some helper functions to handle GUIDs
setGUID: function setGUID(uri, guid) {
uri = uri.spec ? uri.spec : uri;
if (arguments.length == 1)
guid = Utils.makeGUID();
// Ensure annotation name exists
Utils.queryAsync(this._addGUIDAnnotationNameStm);
let stmt = this._checkGUIDPageAnnotationStm;
stmt.params.page_url = uri;
let result = Utils.queryAsync(stmt, ["place_id", "name_id", "anno_id",
"anno_date"])[0];
if (!result) {
let log = Log4Moz.repository.getLogger("Engine.History");
log.warn("Couldn't annotate URI " + uri);
return guid;
}
stmt = this._addPageAnnotationStm;
if (result.anno_id) {
stmt.params.id = result.anno_id;
stmt.params.date_added = result.anno_date;
} else {
stmt.params.id = null;
stmt.params.date_added = Date.now() * 1000;
}
stmt.params.place_id = result.place_id;
stmt.params.name_id = result.name_id;
stmt.params.content = guid;
stmt.params.flags = 0;
stmt.params.expiration = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY;
stmt.params.type = Ci.nsIAnnotationService.TYPE_STRING;
stmt.params.last_modified = Date.now() * 1000;
Utils.queryAsync(stmt);
return guid;
},
get _guidStm() {
let base =
"SELECT a.content AS guid " +
"FROM moz_annos a " +
"JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id ";
let stm;
if (this._haveTempTables) {
// Gecko <2.0
stm = this._getStmt(base +
"JOIN ( " +
"SELECT id FROM moz_places_temp WHERE url = :page_url " +
"UNION " +
"SELECT id FROM moz_places WHERE url = :page_url " +
") AS h ON h.id = a.place_id " +
"WHERE n.name = :anno_name");
} else {
// Gecko 2.0
stm = this._getStmt(base +
"JOIN moz_places h ON h.id = a.place_id " +
"WHERE n.name = :anno_name AND h.url = :page_url");
}
stm.params.anno_name = GUID_ANNO;
return stm;
},
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, ["guid"])[0];
if (result)
return result.guid;
// Give the uri a GUID if it doesn't have one
if (create)
return this.setGUID(uri);
},
get _visitStm() {
// Gecko <2.0
if (this._haveTempTables) {
@ -219,7 +315,7 @@ HistoryStore.prototype = {
},
changeItemID: function HStore_changeItemID(oldID, newID) {
setGUID(this._findURLByGUID(oldID).url, newID);
this.setGUID(this._findURLByGUID(oldID).url, newID);
},
@ -229,8 +325,9 @@ HistoryStore.prototype = {
this._allUrlStm.params.max_results = 5000;
let urls = Utils.queryAsync(this._allUrlStm, "url");
let self = this;
return urls.reduce(function(ids, item) {
ids[GUIDForUri(item.url, true)] = item.url;
ids[self.GUIDForUri(item.url, true)] = item.url;
return ids;
}, {});
},
@ -238,7 +335,7 @@ HistoryStore.prototype = {
create: function HistStore_create(record) {
// Add the url and set the GUID
this.update(record);
setGUID(record.histUri, record.id);
this.setGUID(record.histUri, record.id);
},
remove: function HistStore_remove(record) {
@ -332,6 +429,11 @@ HistoryTracker.prototype = {
}
},
_GUIDForUri: function _GUIDForUri(uri, create) {
// Isn't indirection fun...
return Engines.get("history")._store.GUIDForUri(uri, create);
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsINavHistoryObserver,
Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
@ -354,7 +456,7 @@ HistoryTracker.prototype = {
if (this.ignoreAll)
return;
this._log.trace("onVisit: " + uri.spec);
if (this.addChangedID(GUIDForUri(uri, true)))
if (this.addChangedID(this._GUIDForUri(uri, true)))
this._upScore();
},
onDeleteVisits: function onDeleteVisits() {
@ -365,7 +467,7 @@ HistoryTracker.prototype = {
if (this.ignoreAll)
return;
this._log.trace("onBeforeDeleteURI: " + uri.spec);
if (this.addChangedID(GUIDForUri(uri, true)))
if (this.addChangedID(this._GUIDForUri(uri, true)))
this._upScore();
},
onDeleteURI: function HT_onDeleteURI(uri) {

Просмотреть файл

@ -20,6 +20,7 @@
* Contributor(s):
* Myk Melez <myk@mozilla.org>
* Jono DiCarlo <jdicarlo@mozilla.com>
* Philipp von Weitershausen <philipp@weitershausen.de>
*
* 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
@ -69,6 +70,14 @@ TabEngine.prototype = {
_trackerObj: TabTracker,
_recordObj: TabSetRecord,
getChangedIDs: function getChangedIDs() {
// No need for a proper timestamp (no conflict resolution needed).
let changedIDs = {};
if (this._tracker.modified)
changedIDs[Clients.localID] = 0;
return changedIDs;
},
// API for use by Weave UI code to give user choices of tabs to open:
getAllClients: function TabEngine_getAllClients() {
return this._store._remoteClients;
@ -81,6 +90,7 @@ TabEngine.prototype = {
_resetClient: function TabEngine__resetClient() {
SyncEngine.prototype._resetClient.call(this);
this._store.wipe();
this._tracker.modified = true;
},
/* The intent is not to show tabs in the menu if they're already
@ -237,6 +247,14 @@ TabTracker.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
loadChangedIDs: function loadChangedIDs() {
// Don't read changed IDs from disk at start up.
},
clearChangedIDs: function clearChangedIDs() {
this.modified = false;
},
_topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
_registerListenersForWindow: function registerListenersFW(window) {
this._log.trace("Registering tab listeners in window");
@ -292,7 +310,7 @@ TabTracker.prototype = {
break;
case "private-browsing":
if (aData == "enter" && !PBPrefs.get("autostart"))
this.clearChangedIDs();
this.modified = false;
}
},
@ -303,7 +321,7 @@ TabTracker.prototype = {
}
this._log.trace("onTab event: " + event.type);
this.addChangedID(Clients.localID);
this.modified = true;
// For pageshow events, only give a partial score bump (~.1)
let chance = .1;

Просмотреть файл

@ -20,6 +20,7 @@
* Contributor(s):
* Dan Mills <thunder@mozilla.com>
* Anant Narayanan <anant@kix.in>
* Philipp von Weitershausen <philipp@weitershausen.de>
*
* 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
@ -35,7 +36,7 @@
*
* ***** END LICENSE BLOCK ***** */
const EXPORTED_SYMBOLS = ["Resource"];
const EXPORTED_SYMBOLS = ["Resource", "AsyncResource"];
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -50,24 +51,39 @@ Cu.import("resource://services-sync/ext/Sync.js");
Cu.import("resource://services-sync/log4moz.js");
Cu.import("resource://services-sync/util.js");
// = Resource =
//
// Represents a remote network resource, identified by a URI.
function Resource(uri) {
/*
* AsyncResource represents a remote network resource, identified by a URI.
* Create an instance like so:
*
* let resource = new AsyncResource("http://foobar.com/path/to/resource");
*
* The 'resource' object has the following methods to issue HTTP requests
* of the corresponding HTTP methods:
*
* get(callback)
* put(data, callback)
* post(data, callback)
* delete(callback)
*
* 'callback' is a function with the following signature:
*
* function callback(error, result) {...}
*
* 'error' will be null on successful requests. Likewise, result will not be
* passes (=undefined) when an error occurs. Note that this is independent of
* the status of the HTTP response.
*/
function AsyncResource(uri) {
this._log = Log4Moz.repository.getLogger(this._logName);
this._log.level =
Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")];
this.uri = uri;
this._headers = {};
this._onComplete = Utils.bind2(this, this._onComplete);
}
Resource.prototype = {
AsyncResource.prototype = {
_logName: "Net.Resource",
// ** {{{ Resource.serverTime }}} **
//
// Caches the latest server timestamp (X-Weave-Timestamp header).
serverTime: null,
// ** {{{ Resource.authenticator }}} **
//
// Getter and setter for the authenticator module
@ -137,7 +153,7 @@ Resource.prototype = {
// ** {{{ Resource._createRequest }}} **
//
// This method returns a new IO Channel for requests to be made
// through. It is never called directly, only {{{_request}}} uses it
// through. It is never called directly, only {{{_doRequest}}} uses it
// to obtain a request channel.
//
_createRequest: function Res__createRequest() {
@ -165,14 +181,9 @@ Resource.prototype = {
_onProgress: function Res__onProgress(channel) {},
// ** {{{ 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 iter = 0;
let channel = this._createRequest();
_doRequest: function _doRequest(action, data, callback) {
this._callback = callback;
let channel = this._channel = this._createRequest();
if ("undefined" != typeof(data))
this._data = data;
@ -200,29 +211,21 @@ Resource.prototype = {
// Setup a channel listener so that the actual network operation
// is performed asynchronously.
let [chanOpen, chanCb] = Sync.withCb(channel.asyncOpen, channel);
let listener = new ChannelListener(chanCb, this._onProgress, this._log);
let listener = new ChannelListener(this._onComplete, this._onProgress,
this._log);
channel.requestMethod = action;
channel.asyncOpen(listener, null);
},
// The channel listener might get a failure code
try {
this._data = chanOpen(listener, null);
_onComplete: function _onComplete(error, data) {
if (error) {
this._callback(error);
return;
}
catch(ex) {
// Combine the channel stack with this request stack
let error = Error(ex.message);
let chanStack = [];
if (ex.stack)
chanStack = ex.stack.trim().split(/\n/).slice(1);
let requestStack = error.stack.split(/\n/).slice(1);
// Strip out the args for the last 2 frames because they're usually HUGE!
for (let i = 0; i <= 1; i++)
requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@");
error.stack = chanStack.concat(requestStack).join("\n");
throw error;
}
this._data = data;
let channel = this._channel;
let action = channel.requestMethod;
// Set some default values in-case there's no response header
let headers = {};
@ -240,13 +243,13 @@ Resource.prototype = {
// Log the status of the request
let mesg = [action, success ? "success" : "fail", status,
channel.URI.spec].join(" ");
channel.URI.spec].join(" ");
if (mesg.length > 200)
mesg = mesg.substr(0, 200) + "…";
this._log.debug(mesg);
// Additionally give the full response body when Trace logging
if (this._log.level <= Log4Moz.Level.Trace)
this._log.trace(action + " body: " + this._data);
this._log.trace(action + " body: " + data);
// This is a server-side safety valve to allow slowing down
// clients without hurting performance.
@ -263,7 +266,7 @@ Resource.prototype = {
this._log.debug(action + " cached: " + status);
}
let ret = new String(this._data);
let ret = new String(data);
ret.headers = headers;
ret.status = status;
ret.success = success;
@ -284,22 +287,96 @@ Resource.prototype = {
// Do the same type of request but with the new uri
if (subject.newUri != "") {
this.uri = subject.newUri;
return this._request.apply(this, arguments);
this._doRequest(action, this._data, this._callback);
return;
}
}
return ret;
this._callback(null, ret);
},
get: function get(callback) {
this._doRequest("GET", undefined, callback);
},
put: function put(data, callback) {
if (typeof data == "function")
[data, callback] = [undefined, data];
this._doRequest("PUT", data, callback);
},
post: function post(data, callback) {
if (typeof data == "function")
[data, callback] = [undefined, data];
this._doRequest("POST", data, callback);
},
delete: function delete(callback) {
this._doRequest("DELETE", undefined, callback);
}
};
/*
* Represent a remote network resource, identified by a URI, with a
* synchronous API.
*
* 'Resource' is not recommended for new code. Use the asynchronous API of
* 'AsyncResource' instead.
*/
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 [doRequest, cb] = Sync.withCb(this._doRequest, this);
function callback(error, ret) {
if (error)
cb.throw(error);
cb(ret);
}
// The channel listener might get a failure code
try {
return doRequest(action, data, callback);
} catch(ex) {
// Combine the channel stack with this request stack
let error = Error(ex.message);
let chanStack = [];
if (ex.stack)
chanStack = ex.stack.trim().split(/\n/).slice(1);
let requestStack = error.stack.split(/\n/).slice(1);
// Strip out the args for the last 2 frames because they're usually HUGE!
for (let i = 0; i <= 1; i++)
requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@");
error.stack = chanStack.concat(requestStack).join("\n");
throw error;
}
},
// ** {{{ Resource.get }}} **
//
// Perform an asynchronous HTTP GET for this resource.
// onComplete will be called on completion of the request.
get: function Res_get() {
return this._request("GET");
},
// ** {{{ Resource.get }}} **
// ** {{{ Resource.put }}} **
//
// Perform a HTTP PUT for this resource.
put: function Res_put(data) {
@ -357,10 +434,12 @@ ChannelListener.prototype = {
this._data = null;
// Throw the failure code name (and stop execution)
if (!Components.isSuccessCode(status))
this._onComplete.throw(Error(Components.Exception("", status).name));
if (!Components.isSuccessCode(status)) {
this._onComplete(Error(Components.Exception("", status).name));
return;
}
this._onComplete(this._data);
this._onComplete(null, this._data);
},
onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) {
@ -384,7 +463,7 @@ ChannelListener.prototype = {
// Ignore any callbacks if we happen to get any now
this.onStopRequest = function() {};
this.onDataAvailable = function() {};
this._onComplete.throw(Error("Aborting due to channel inactivity."));
this._onComplete(Error("Aborting due to channel inactivity."));
}
};

Просмотреть файл

@ -53,6 +53,36 @@ Cu.import("resource://services-sync/log4moz.js");
*/
let Utils = {
/**
* Execute an arbitrary number of asynchronous functions one after the
* other, passing the callback arguments on to the next one. All functions
* must take a callback function as their last argument. The 'this' object
* will be whatever asyncChain's is.
*
* @usage this._chain = Utils.asyncChain;
* this._chain(this.foo, this.bar, this.baz)(args, for, foo)
*
* This is equivalent to:
*
* let self = this;
* self.foo(args, for, foo, function (bars, args) {
* self.bar(bars, args, function (baz, params) {
* self.baz(baz, params);
* });
* });
*/
asyncChain: function asyncChain() {
let funcs = Array.slice(arguments);
let thisObj = this;
return function callback() {
if (funcs.length) {
let args = Array.slice(arguments).concat(callback);
let f = funcs.shift();
f.apply(thisObj, args);
}
};
},
/**
* Wrap a function to catch all exceptions and log them
*

Просмотреть файл

@ -20,6 +20,7 @@ pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyc
pref("services.sync.log.appender.console", "Warn");
pref("services.sync.log.appender.dump", "Error");
pref("services.sync.log.appender.debugLog", "Trace");
pref("services.sync.log.appender.debugLog.enabled", false);
pref("services.sync.log.rootLogger", "Debug");
pref("services.sync.log.logger.service.main", "Debug");
pref("services.sync.log.logger.authenticator", "Debug");

Просмотреть файл

@ -42,6 +42,13 @@ function run_test() {
_("The calculated sort index is based on frecency data.");
do_check_true(newrecord.sortindex >= 150);
_("Folders have high sort index to ensure they're synced first.");
let folder_id = Svc.Bookmark.createFolder(Svc.Bookmark.toolbarFolder,
"Test Folder", 0);
let folder_guid = Svc.Bookmark.getItemGUID(folder_id);
let folder_record = store.createRecord(folder_guid, "http://fake/uri");
do_check_eq(folder_record.sortindex, 1000000);
} finally {
_("Clean up.");
store.wipe();

Просмотреть файл

@ -1,7 +1,6 @@
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/engines/history.js");
Cu.import("resource://services-sync/type_records/history.js");
Cu.import("resource://services-sync/ext/Sync.js");
Cu.import("resource://services-sync/util.js");
const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
@ -28,30 +27,40 @@ function queryHistoryVisits(uri) {
return queryPlaces(uri, options);
}
function waitForTitleChanged(test) {
Sync(function (callback) {
Svc.History.addObserver({
onBeginUpdateBatch: function onBeginUpdateBatch() {},
onEndUpdateBatch: function onEndUpdateBatch() {},
onPageChanged: function onPageChanged() {},
onTitleChanged: function onTitleChanged() {
Svc.History.removeObserver(this);
callback();
},
onVisit: function onVisit() {},
onDeleteVisits: function onDeleteVisits() {},
onPageExpired: function onPageExpired() {},
onBeforeDeleteURI: function onBeforeDeleteURI() {},
onDeleteURI: function onDeleteURI() {},
onClearHistory: function onClearHistory() {},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsINavHistoryObserver,
Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
Ci.nsISupportsWeakReference
])
}, true);
test();
})();
function onNextTitleChanged(callback) {
Svc.History.addObserver({
onBeginUpdateBatch: function onBeginUpdateBatch() {},
onEndUpdateBatch: function onEndUpdateBatch() {},
onPageChanged: function onPageChanged() {},
onTitleChanged: function onTitleChanged() {
Svc.History.removeObserver(this);
Utils.delay(callback, 0, this);
},
onVisit: function onVisit() {},
onDeleteVisits: function onDeleteVisits() {},
onPageExpired: function onPageExpired() {},
onBeforeDeleteURI: function onBeforeDeleteURI() {},
onDeleteURI: function onDeleteURI() {},
onClearHistory: function onClearHistory() {},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsINavHistoryObserver,
Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
Ci.nsISupportsWeakReference
])
}, true);
}
// Ensure exceptions from inside callbacks leads to test failures while
// we still clean up properly.
function ensureThrows(func) {
return function() {
try {
func.apply(this, arguments);
} catch (ex) {
Svc.History.removeAllPages();
do_throw(ex);
}
};
}
function run_test() {
@ -59,15 +68,18 @@ function run_test() {
let store = new HistoryEngine()._store;
do_check_eq([id for (id in store.getAllIDs())].length, 0);
try {
let fxuri, fxguid, tburi, tbguid;
do_test_pending();
Utils.asyncChain(function (next) {
_("Let's create an entry in the database.");
let fxuri = Utils.makeURI("http://getfirefox.com/");
fxuri = Utils.makeURI("http://getfirefox.com/");
Svc.History.addPageWithDetails(fxuri, "Get Firefox!", TIMESTAMP1);
_("Verify that the entry exists.");
let ids = [id for (id in store.getAllIDs())];
do_check_eq(ids.length, 1);
let fxguid = ids[0];
fxguid = ids[0];
do_check_true(store.itemExists(fxguid));
_("If we query a non-existent record, it's marked as deleted.");
@ -85,36 +97,43 @@ function run_test() {
_("Let's modify the record and have the store update the database.");
let secondvisit = {date: TIMESTAMP2,
type: Ci.nsINavHistoryService.TRANSITION_TYPED};
waitForTitleChanged(function() {
store.update({histUri: record.histUri,
title: "Hol Dir Firefox!",
visits: [record.visits[0], secondvisit]});
});
let queryres = queryHistoryVisits(fxuri);
do_check_eq(queryres.length, 2);
do_check_eq(queryres[0].time, TIMESTAMP1);
do_check_eq(queryres[0].title, "Hol Dir Firefox!");
do_check_eq(queryres[1].time, TIMESTAMP2);
do_check_eq(queryres[1].title, "Hol Dir Firefox!");
onNextTitleChanged(ensureThrows(function() {
let queryres = queryHistoryVisits(fxuri);
do_check_eq(queryres.length, 2);
do_check_eq(queryres[0].time, TIMESTAMP1);
do_check_eq(queryres[0].title, "Hol Dir Firefox!");
do_check_eq(queryres[1].time, TIMESTAMP2);
do_check_eq(queryres[1].title, "Hol Dir Firefox!");
next();
}));
store.update({histUri: record.histUri,
title: "Hol Dir Firefox!",
visits: [record.visits[0], secondvisit]});
}, function (next) {
_("Create a brand new record through the store.");
let tbguid = Utils.makeGUID();
let tburi = Utils.makeURI("http://getthunderbird.com");
waitForTitleChanged(function() {
store.create({id: tbguid,
histUri: tburi.spec,
title: "The bird is the word!",
visits: [{date: TIMESTAMP3,
type: Ci.nsINavHistoryService.TRANSITION_TYPED}]});
});
do_check_eq([id for (id in store.getAllIDs())].length, 2);
queryres = queryHistoryVisits(tburi);
do_check_eq(queryres.length, 1);
do_check_eq(queryres[0].time, TIMESTAMP3);
do_check_eq(queryres[0].title, "The bird is the word!");
tbguid = Utils.makeGUID();
tburi = Utils.makeURI("http://getthunderbird.com");
onNextTitleChanged(ensureThrows(function() {
do_check_eq([id for (id in store.getAllIDs())].length, 2);
let queryres = queryHistoryVisits(tburi);
do_check_eq(queryres.length, 1);
do_check_eq(queryres[0].time, TIMESTAMP3);
do_check_eq(queryres[0].title, "The bird is the word!");
next();
}));
store.create({id: tbguid,
histUri: tburi.spec,
title: "The bird is the word!",
visits: [{date: TIMESTAMP3,
type: Ci.nsINavHistoryService.TRANSITION_TYPED}]});
}, function (next) {
_("Make sure we handle invalid URLs in places databases gracefully.");
let query = "INSERT INTO moz_places "
let table = store._haveTempTables ? "moz_places_temp" : "moz_places";
let query = "INSERT INTO " + table + " "
+ "(url, title, rev_host, visit_count, last_visit_date) "
+ "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")";
let stmt = Utils.createStatement(Svc.History.DBConnection, query);
@ -124,7 +143,7 @@ function run_test() {
_("Remove a record from the store.");
store.remove({id: fxguid});
do_check_false(store.itemExists(fxguid));
queryres = queryHistoryVisits(fxuri);
let queryres = queryHistoryVisits(fxuri);
do_check_eq(queryres.length, 0);
_("Make sure wipe works.");
@ -135,8 +154,10 @@ function run_test() {
queryres = queryHistoryVisits(tburi);
do_check_eq(queryres.length, 0);
} finally {
_("Clean up.");
Svc.History.removeAllPages();
}
do_test_finished();
})();
}

Просмотреть файл

@ -1,10 +1,13 @@
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/history.js");
Cu.import("resource://services-sync/util.js");
function run_test() {
Engines.register(HistoryEngine);
let tracker = Engines.get("history")._tracker;
_("Verify we've got an empty tracker to work with.");
let tracker = new HistoryEngine()._tracker;
do_check_eq([id for (id in tracker.changedIDs)].length, 0);
let _counter = 0;

Просмотреть файл

@ -0,0 +1,629 @@
Cu.import("resource://services-sync/auth.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");
let logger;
function server_open(metadata, response) {
let body;
if (metadata.method == "GET") {
body = "This path exists";
response.setStatusLine(metadata.httpVersion, 200, "OK");
} else {
body = "Wrong request method";
response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
}
response.bodyOutputStream.write(body, body.length);
}
function server_protected(metadata, response) {
let body;
// no btoa() in xpcshell. it's guest:guest
if (metadata.hasHeader("Authorization") &&
metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
body = "This path exists and is protected";
response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
} else {
body = "This path exists and is protected - failed";
response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
}
response.bodyOutputStream.write(body, body.length);
}
function server_404(metadata, response) {
let body = "File not found";
response.setStatusLine(metadata.httpVersion, 404, "Not Found");
response.bodyOutputStream.write(body, body.length);
}
let sample_data = {
some: "sample_data",
injson: "format",
number: 42
};
function server_upload(metadata, response) {
let body;
let input = readBytesFromInputStream(metadata.bodyInputStream);
if (input == JSON.stringify(sample_data)) {
body = "Valid data upload via " + metadata.method;
response.setStatusLine(metadata.httpVersion, 200, "OK");
} else {
body = "Invalid data upload via " + metadata.method + ': ' + input;
response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error");
}
response.bodyOutputStream.write(body, body.length);
}
function server_delete(metadata, response) {
let body;
if (metadata.method == "DELETE") {
body = "This resource has been deleted";
response.setStatusLine(metadata.httpVersion, 200, "OK");
} else {
body = "Wrong request method";
response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
}
response.bodyOutputStream.write(body, body.length);
}
function server_json(metadata, response) {
let body = JSON.stringify(sample_data);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
const TIMESTAMP = 1274380461;
function server_timestamp(metadata, response) {
let body = "Thank you for your request";
response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function server_backoff(metadata, response) {
let body = "Hey, back off!";
response.setHeader("X-Weave-Backoff", '600', false);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function server_quota_notice(request, response) {
let body = "You're approaching quota.";
response.setHeader("X-Weave-Quota-Remaining", '1048576', false);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function server_quota_error(request, response) {
let body = "14";
response.setHeader("X-Weave-Quota-Remaining", '-1024', false);
response.setStatusLine(request.httpVersion, 400, "OK");
response.bodyOutputStream.write(body, body.length);
}
function server_headers(metadata, response) {
let ignore_headers = ["host", "user-agent", "accept", "accept-language",
"accept-encoding", "accept-charset", "keep-alive",
"connection", "pragma", "cache-control",
"content-length"];
let headers = metadata.headers;
let header_names = [];
while (headers.hasMoreElements()) {
let header = headers.getNext().toString();
if (ignore_headers.indexOf(header) == -1) {
header_names.push(header);
}
}
header_names = header_names.sort();
headers = {};
for each (let header in header_names) {
headers[header] = metadata.getHeader(header);
}
let body = JSON.stringify(headers);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function run_test() {
logger = Log4Moz.repository.getLogger('Test');
Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
do_test_pending();
let server = httpd_setup({
"/open": server_open,
"/protected": server_protected,
"/404": server_404,
"/upload": server_upload,
"/delete": server_delete,
"/json": server_json,
"/timestamp": server_timestamp,
"/headers": server_headers,
"/backoff": server_backoff,
"/quota-notice": server_quota_notice,
"/quota-error": server_quota_error
});
Utils.prefs.setIntPref("network.numRetries", 1); // speed up test
let did401 = false;
Observers.add("weave:resource:status:401", function() did401 = true);
let quotaValue;
Observers.add("weave:service:quota:remaining",
function (subject) quotaValue = subject);
// Ensure exceptions from inside callbacks leads to test failures.
function ensureThrows(func) {
return function() {
try {
func.apply(this, arguments);
} catch (ex) {
do_throw(ex);
}
};
}
let res_upload = new AsyncResource("http://localhost:8080/upload");
let res_headers = new AsyncResource("http://localhost:8080/headers");
_("Resource object memebers");
let res = new AsyncResource("http://localhost:8080/open");
do_check_true(res.uri instanceof Ci.nsIURI);
do_check_eq(res.uri.spec, "http://localhost:8080/open");
do_check_eq(res.spec, "http://localhost:8080/open");
do_check_eq(typeof res.headers, "object");
do_check_eq(typeof res.authenticator, "object");
// Initially res.data is null since we haven't performed a GET or
// PUT/POST request yet.
do_check_eq(res.data, null);
Utils.asyncChain(function (next) {
_("GET a non-password-protected resource");
do_test_pending();
res.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "This path exists");
do_check_eq(content.status, 200);
do_check_true(content.success);
// res.data has been updated with the result from the request
do_check_eq(res.data, content);
// Since we didn't receive proper JSON data, accessing content.obj
// will result in a SyntaxError from JSON.parse
let didThrow = false;
try {
content.obj;
} catch (ex) {
didThrow = true;
}
do_check_true(didThrow);
do_test_finished();
next();
}));
}, function (next) {
_("GET a password protected resource (test that it'll fail w/o pass, no throw)");
let res2 = new AsyncResource("http://localhost:8080/protected");
do_test_pending();
res2.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_true(did401);
do_check_eq(content, "This path exists and is protected - failed");
do_check_eq(content.status, 401);
do_check_false(content.success);
do_test_finished();
next();
}));
}, function (next) {
_("GET a password protected resource");
let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
let res3 = new AsyncResource("http://localhost:8080/protected");
res3.authenticator = auth;
do_check_eq(res3.authenticator, auth);
do_test_pending();
res3.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "This path exists and is protected");
do_check_eq(content.status, 200);
do_check_true(content.success);
do_test_finished();
next();
}));
}, function (next) {
_("GET a non-existent resource (test that it'll fail, but not throw)");
let res4 = new AsyncResource("http://localhost:8080/404");
do_test_pending();
res4.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "File not found");
do_check_eq(content.status, 404);
do_check_false(content.success);
// Check some headers of the 404 response
do_check_eq(content.headers.connection, "close");
do_check_eq(content.headers.server, "httpd.js");
do_check_eq(content.headers["content-length"], 14);
do_test_finished();
next();
}));
}, function (next) {
_("PUT to a resource (string)");
do_test_pending();
res_upload.put(JSON.stringify(sample_data), ensureThrows(function(error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("PUT to a resource (object)");
do_test_pending();
res_upload.put(sample_data, ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("PUT without data arg (uses resource.data) (string)");
do_test_pending();
res_upload.data = JSON.stringify(sample_data);
res_upload.put(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("PUT without data arg (uses resource.data) (object)");
do_test_pending();
res_upload.data = sample_data;
res_upload.put(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("POST to a resource (string)");
do_test_pending();
res_upload.post(JSON.stringify(sample_data), ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("POST to a resource (object)");
do_test_pending();
res_upload.post(sample_data, ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("POST without data arg (uses resource.data) (string)");
do_test_pending();
res_upload.data = JSON.stringify(sample_data);
res_upload.post(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("POST without data arg (uses resource.data) (object)");
do_test_pending();
res_upload.data = sample_data;
res_upload.post(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res_upload.data, content);
do_test_finished();
next();
}));
}, function (next) {
_("DELETE a resource");
do_test_pending();
let res6 = new AsyncResource("http://localhost:8080/delete");
res6.delete(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "This resource has been deleted");
do_check_eq(content.status, 200);
do_test_finished();
next();
}));
}, function (next) {
_("JSON conversion of response body");
do_test_pending();
let res7 = new AsyncResource("http://localhost:8080/json");
res7.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
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));
do_test_finished();
next();
}));
}, function (next) {
_("X-Weave-Timestamp header updates Resource.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);
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_test_finished();
next();
}));
}, function (next) {
_("GET: no special request headers");
do_test_pending();
res_headers.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, '{}');
do_test_finished();
next();
}));
}, function (next) {
_("PUT: Content-Type defaults to text/plain");
do_test_pending();
res_headers.put('data', ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
do_test_finished();
next();
}));
}, function (next) {
_("POST: Content-Type defaults to text/plain");
do_test_pending();
res_headers.post('data', ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
do_test_finished();
next();
}));
}, function (next) {
_("setHeader(): setting simple header");
do_test_pending();
res_headers.setHeader('X-What-Is-Weave', 'awesome');
do_check_eq(res_headers.headers['x-what-is-weave'], 'awesome');
res_headers.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"}));
do_test_finished();
next();
}));
}, function (next) {
_("setHeader(): setting multiple headers, overwriting existing header");
do_test_pending();
res_headers.setHeader('X-WHAT-is-Weave', 'more awesomer',
'X-Another-Header', 'hello world');
do_check_eq(res_headers.headers['x-what-is-weave'], 'more awesomer');
do_check_eq(res_headers.headers['x-another-header'], 'hello world');
res_headers.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"x-another-header": "hello world",
"x-what-is-weave": "more awesomer"}));
do_test_finished();
next();
}));
}, function (next) {
_("Setting headers object");
do_test_pending();
res_headers.headers = {};
res_headers.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "{}");
do_test_finished();
next();
}));
}, function (next) {
_("PUT: override default Content-Type");
do_test_pending();
res_headers.setHeader('Content-Type', 'application/foobar');
do_check_eq(res_headers.headers['content-type'], 'application/foobar');
res_headers.put('data', ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
do_test_finished();
next();
}));
}, function (next) {
_("POST: override default Content-Type");
do_test_pending();
res_headers.post('data', ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
do_test_finished();
next();
}));
}, function (next) {
_("X-Weave-Backoff header notifies observer");
let backoffInterval;
function onBackoff(subject, data) {
backoffInterval = subject;
}
Observers.add("weave:service:backoff:interval", onBackoff);
do_test_pending();
let res10 = new AsyncResource("http://localhost:8080/backoff");
res10.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(backoffInterval, 600);
do_test_finished();
next();
}));
}, function (next) {
_("X-Weave-Quota-Remaining header notifies observer on successful requests.");
do_test_pending();
let res10 = new AsyncResource("http://localhost:8080/quota-error");
res10.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content.status, 400);
do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification.
do_test_finished();
next();
}));
}, function (next) {
do_test_pending();
let res10 = new AsyncResource("http://localhost:8080/quota-notice");
res10.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(content.status, 200);
do_check_eq(quotaValue, 1048576);
do_test_finished();
next();
}));
}, function (next) {
_("Error handling in ChannelListener etc. preserves exception information");
do_test_pending();
let res11 = new AsyncResource("http://localhost:12345/does/not/exist");
res11.get(ensureThrows(function (error, content) {
do_check_neq(error, null);
do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
do_check_eq(typeof error.stack, "string");
do_test_finished();
next();
}));
}, function (next) {
let redirRequest;
let redirToOpen = function(subject) {
subject.newUri = "http://localhost:8080/open";
redirRequest = subject;
};
Observers.add("weave:resource:status:401", redirToOpen);
_("Notification of 401 can redirect to another uri");
did401 = false;
do_test_pending();
let res12 = new AsyncResource("http://localhost:8080/protected");
res12.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_eq(res12.spec, "http://localhost:8080/open");
do_check_eq(content, "This path exists");
do_check_eq(content.status, 200);
do_check_true(content.success);
do_check_eq(res.data, content);
do_check_true(did401);
do_check_eq(redirRequest.response, "This path exists and is protected - failed");
do_check_eq(redirRequest.response.status, 401);
do_check_false(redirRequest.response.success);
Observers.remove("weave:resource:status:401", redirToOpen);
do_test_finished();
next();
}));
}, function (next) {
_("Removing the observer should result in the original 401");
did401 = false;
let res13 = new AsyncResource("http://localhost:8080/protected");
res13.get(ensureThrows(function (error, content) {
do_check_eq(error, null);
do_check_true(did401);
do_check_eq(content, "This path exists and is protected - failed");
do_check_eq(content.status, 401);
do_check_false(content.success);
do_test_finished();
next();
}));
}, function (next) {
// Don't quit test harness before server shuts down.
server.stop(do_test_finished);
})();
}

Просмотреть файл

@ -43,18 +43,25 @@ function test_syncID() {
}
function test_lastSync() {
_("SyncEngine.lastSync corresponds to preference and stores floats");
_("SyncEngine.lastSync and SyncEngine.lastSyncLocal correspond to preferences");
let engine = makeSteamEngine();
try {
// Ensure pristine environment
do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined);
do_check_eq(engine.lastSync, 0);
do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), undefined);
do_check_eq(engine.lastSyncLocal, 0);
// Floats are properly stored as floats and synced with the preference
engine.lastSync = 123.45;
do_check_eq(engine.lastSync, 123.45);
do_check_eq(Svc.Prefs.get("steam.lastSync"), "123.45");
// Integer is properly stored
engine.lastSyncLocal = 67890;
do_check_eq(engine.lastSyncLocal, 67890);
do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), "67890");
// resetLastSync() resets the value (and preference) to 0
engine.resetLastSync();
do_check_eq(engine.lastSync, 0);
@ -64,48 +71,20 @@ function test_lastSync() {
}
}
function test_toFetch() {
_("SyncEngine.toFetch corresponds to file on disk");
let engine = makeSteamEngine();
try {
// Ensure pristine environment
do_check_eq(engine.toFetch.length, 0);
// Write file to disk
let toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
engine.toFetch = toFetch;
do_check_eq(engine.toFetch, toFetch);
let fakefile = syncTesting.fakeFilesystem.fakeContents[
"weave/toFetch/steam.json"];
do_check_eq(fakefile, JSON.stringify(toFetch));
// Read file from disk
toFetch = [Utils.makeGUID(), Utils.makeGUID()];
syncTesting.fakeFilesystem.fakeContents["weave/toFetch/steam.json"]
= JSON.stringify(toFetch);
engine.loadToFetch();
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_resetClient() {
_("SyncEngine.resetClient resets lastSync and toFetch");
_("SyncEngine.resetClient resets lastSync");
let engine = makeSteamEngine();
try {
// Ensure pristine environment
do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined);
do_check_eq(engine.toFetch.length, 0);
do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), undefined);
engine.toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
engine.lastSync = 123.45;
engine.lastSyncLocal = 67890;
engine.resetClient();
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.toFetch.length, 0);
do_check_eq(engine.lastSyncLocal, 0);
} finally {
syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
Svc.Prefs.resetBranch("");
@ -128,14 +107,12 @@ function test_wipeServer() {
try {
// Some data to reset.
engine.toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
engine.lastSync = 123.45;
_("Wipe server data and reset client.");
engine.wipeServer(true);
do_check_eq(steamCollection.payload, undefined);
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.toFetch.length, 0);
_("We passed a truthy arg earlier in which case it doesn't wipe the crypto collection.");
do_check_eq(steamCrypto.payload, PAYLOAD);
@ -153,7 +130,6 @@ function run_test() {
test_url_attributes();
test_syncID();
test_lastSync();
test_toFetch();
test_resetClient();
test_wipeServer();
}

Просмотреть файл

@ -181,6 +181,7 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
do_check_true(!!collection.wbos.scotsman.payload);
engine.lastSync = Date.now() / 1000;
engine.lastSyncLocal = Date.now();
engine._syncStartup();
// The meta/global WBO has been filled with data about the engine
@ -190,6 +191,7 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
// Sync was reset and server data was wiped
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.lastSyncLocal, 0);
do_check_eq(collection.wbos.flying.payload, undefined);
do_check_eq(collection.wbos.scotsman.payload, undefined);
@ -197,9 +199,6 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
do_check_true(!!crypto_steam.payload);
do_check_true(!!crypto_steam.data.keyring);
// WBO IDs are added to tracker (they're all marked for uploading)
do_check_eq(engine._tracker.changedIDs["rekolok"], 0);
} finally {
server.stop(do_test_finished);
Svc.Prefs.resetBranch("");
@ -248,10 +247,12 @@ function test_syncStartup_metaGet404() {
do_check_true(!!collection.wbos.scotsman.payload);
engine.lastSync = Date.now() / 1000;
engine.lastSyncLocal = Date.now();
engine._syncStartup();
_("Sync was reset and server data was wiped");
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.lastSyncLocal, 0);
do_check_eq(collection.wbos.flying.payload, undefined);
do_check_eq(collection.wbos.scotsman.payload, undefined);
@ -363,6 +364,7 @@ function test_syncStartup_syncIDMismatchResetsClient() {
do_check_eq(engine._tracker.changedIDs["rekolok"], undefined);
engine.lastSync = Date.now() / 1000;
engine.lastSyncLocal = Date.now();
engine._syncStartup();
// The engine has assumed the server's syncID
@ -370,6 +372,7 @@ function test_syncStartup_syncIDMismatchResetsClient() {
// Sync was reset
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.lastSyncLocal, 0);
} finally {
server.stop(do_test_finished);
@ -430,10 +433,12 @@ function test_syncStartup_badKeyWipesServerData() {
do_check_true(!!collection.wbos.scotsman.payload);
engine.lastSync = Date.now() / 1000;
engine.lastSyncLocal = Date.now();
engine._syncStartup();
// Sync was reset and server data was wiped
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.lastSyncLocal, 0);
do_check_eq(collection.wbos.flying.payload, undefined);
do_check_eq(collection.wbos.scotsman.payload, undefined);
@ -473,7 +478,6 @@ function test_processIncoming_emptyServer() {
// Merely ensure that this code path is run without any errors
engine._processIncoming();
do_check_eq(engine.lastSync, 0);
do_check_eq(engine.toFetch.length, 0);
} finally {
server.stop(do_test_finished);
@ -521,6 +525,10 @@ function test_processIncoming_createFromServer() {
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
let engine = makeSteamEngine();
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = {steam: {version: engine.version,
syncID: engine.syncID}};
try {
// Confirm initial environment
@ -531,6 +539,7 @@ function test_processIncoming_createFromServer() {
do_check_eq(engine._store.items['../pathological'], undefined);
do_check_eq(engine._store.items.wrong_keyuri, undefined);
engine._syncStartup();
engine._processIncoming();
// Timestamps of last sync and last server modification are set.
@ -626,6 +635,10 @@ function test_processIncoming_reconcile() {
// This record has been changed 2 mins later than the one on the server
engine._tracker.addChangedID('olderidentical', Date.now()/1000);
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = {steam: {version: engine.version,
syncID: engine.syncID}};
try {
// Confirm initial environment
@ -636,7 +649,7 @@ function test_processIncoming_reconcile() {
do_check_eq(engine._store.items.nukeme, "Nuke me!");
do_check_true(engine._tracker.changedIDs['olderidentical'] > 0);
engine._delete = {}; // normally set up by _syncStartup
engine._syncStartup();
engine._processIncoming();
// Timestamps of last sync and last server modification are set.
@ -676,14 +689,22 @@ function test_processIncoming_reconcile() {
}
function test_processIncoming_fetchNum() {
_("SyncEngine._processIncoming doesn't fetch everything at ones on mobile clients");
function test_processIncoming_mobile_batchSize() {
_("SyncEngine._processIncoming doesn't fetch everything at once on mobile clients");
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
Svc.Prefs.set("username", "foo");
Svc.Prefs.set("client.type", "mobile");
let crypto_steam = new ServerWBO('steam');
// A collection that logs each GET
let collection = new ServerCollection();
collection.get_log = [];
collection._get = collection.get;
collection.get = function (options) {
this.get_log.push(options);
return this._get(options);
};
// Let's create some 234 server side records. They're all at least
// 10 minutes old.
@ -704,75 +725,38 @@ function test_processIncoming_fetchNum() {
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
let engine = makeSteamEngine();
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = {steam: {version: engine.version,
syncID: engine.syncID}};
try {
// On a mobile client, the first sync will only get the first 50
// objects from the server
// On a mobile client, we get new records from the server in batches of 50.
engine._syncStartup();
engine._processIncoming();
do_check_eq([id for (id in engine._store.items)].length, 50);
do_check_eq([id for (id in engine._store.items)].length, 234);
do_check_true('record-no-0' in engine._store.items);
do_check_true('record-no-49' in engine._store.items);
do_check_eq(engine.toFetch.length, 234 - 50);
// The next sync will get another 50 objects, assuming the server
// hasn't got any new data.
engine._processIncoming();
do_check_eq([id for (id in engine._store.items)].length, 100);
do_check_true('record-no-50' in engine._store.items);
do_check_true('record-no-99' in engine._store.items);
do_check_eq(engine.toFetch.length, 234 - 100);
// Now let's say there are some new items on the server
for (i=0; i < 5; i++) {
let id = 'new-record-no-' + i;
let payload = encryptPayload({id: id, denomination: "New record No. " + i});
let wbo = new ServerWBO(id, payload);
wbo.modified = Date.now()/1000 - 60*i;
collection.wbos[id] = wbo;
}
// Let's tell the engine the server has got newer data. This is
// normally done by the WeaveSvc after retrieving info/collections.
engine.lastModified = Date.now() / 1000 + 1;
// Now we'll fetch another 50 items, but 5 of those are the new
// ones, so we've only fetched another 45 of the older ones.
engine._processIncoming();
do_check_eq([id for (id in engine._store.items)].length, 150);
do_check_true('new-record-no-0' in engine._store.items);
do_check_true('new-record-no-4' in engine._store.items);
do_check_true('record-no-100' in engine._store.items);
do_check_true('record-no-144' in engine._store.items);
do_check_eq(engine.toFetch.length, 234 - 100 - 45);
// Now let's modify a few existing records on the server so that
// they have to be refetched.
collection.wbos['record-no-3'].modified = Date.now()/1000 + 1;
collection.wbos['record-no-41'].modified = Date.now()/1000 + 1;
collection.wbos['record-no-122'].modified = Date.now()/1000 + 1;
// Once again we'll tell the engine that the server's got newer data
// and once again we'll fetch 50 items, but 3 of those are the
// existing records, so we're only fetching 47 new ones.
engine.lastModified = Date.now() / 1000 + 2;
engine._processIncoming();
do_check_eq([id for (id in engine._store.items)].length, 197);
do_check_true('record-no-145' in engine._store.items);
do_check_true('record-no-191' in engine._store.items);
do_check_eq(engine.toFetch.length, 234 - 100 - 45 - 47);
// Finally let's fetch the rest, making sure that will fetch
// everything up to the last record.
while(engine.toFetch.length) {
engine._processIncoming();
}
do_check_eq([id for (id in engine._store.items)].length, 234 + 5);
do_check_true('record-no-233' in engine._store.items);
// Verify that the right number of GET requests with the right
// kind of parameters were made.
do_check_eq(collection.get_log.length,
Math.ceil(234 / MOBILE_BATCH_SIZE) + 1);
do_check_eq(collection.get_log[0].full, 1);
do_check_eq(collection.get_log[0].limit, MOBILE_BATCH_SIZE);
do_check_eq(collection.get_log[1].full, undefined);
do_check_eq(collection.get_log[1].limit, undefined);
for (let i = 1; i <= Math.floor(234 / MOBILE_BATCH_SIZE); i++) {
do_check_eq(collection.get_log[i+1].full, 1);
do_check_eq(collection.get_log[i+1].limit, undefined);
if (i < Math.floor(234 / MOBILE_BATCH_SIZE))
do_check_eq(collection.get_log[i+1].ids.length, MOBILE_BATCH_SIZE);
else
do_check_eq(collection.get_log[i+1].ids.length, 234 % MOBILE_BATCH_SIZE);
}
} finally {
server.stop(do_test_finished);
Svc.Prefs.resetBranch("");
@ -804,20 +788,29 @@ function test_uploadOutgoing_toEmptyServer() {
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
let engine = makeSteamEngine();
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
engine._store.items = {flying: "LNER Class A3 4472",
scotsman: "Flying Scotsman"};
// Mark one of these records as changed
engine._tracker.addChangedID('scotsman', 0);
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = {steam: {version: engine.version,
syncID: engine.syncID}};
try {
// Confirm initial environment
do_check_eq(engine.lastSyncLocal, 0);
do_check_eq(collection.wbos.flying.payload, undefined);
do_check_eq(collection.wbos.scotsman.payload, undefined);
do_check_eq(engine._tracker.changedIDs['scotsman'], 0);
engine._syncStartup();
engine._uploadOutgoing();
// Local timestamp has been set.
do_check_true(engine.lastSyncLocal > 0);
// Ensure the marked record ('scotsman') has been uploaded and is
// no longer marked.
do_check_eq(collection.wbos.flying.payload, undefined);
@ -859,10 +852,11 @@ function test_uploadOutgoing_failed() {
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
let engine = makeSteamEngine();
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
engine._store.items = {flying: "LNER Class A3 4472",
scotsman: "Flying Scotsman",
peppercorn: "Peppercorn Class"};
// Mark one of these records as changed
// Mark these records as changed
const FLYING_CHANGED = 12345;
const SCOTSMAN_CHANGED = 23456;
const PEPPERCORN_CHANGED = 34567;
@ -870,16 +864,25 @@ function test_uploadOutgoing_failed() {
engine._tracker.addChangedID('scotsman', SCOTSMAN_CHANGED);
engine._tracker.addChangedID('peppercorn', PEPPERCORN_CHANGED);
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = {steam: {version: engine.version,
syncID: engine.syncID}};
try {
// Confirm initial environment
do_check_eq(engine.lastSyncLocal, 0);
do_check_eq(collection.wbos.flying.payload, undefined);
do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
engine._syncStartup();
engine._uploadOutgoing();
// Local timestamp has been set.
do_check_true(engine.lastSyncLocal > 0);
// Ensure the 'flying' record has been uploaded and is no longer marked.
do_check_true(!!collection.wbos.flying.payload);
do_check_eq(engine._tracker.changedIDs['flying'], undefined);
@ -925,6 +928,10 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
collection.wbos[id] = new ServerWBO(id);
}
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.0/foo/storage/crypto/steam": crypto_steam.handler(),
"/1.0/foo/storage/steam": collection.handler()
@ -938,6 +945,7 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
// Confirm initial environment
do_check_eq(noOfUploads, 0);
engine._syncStartup();
engine._uploadOutgoing();
// Ensure all records have been uploaded
@ -1087,6 +1095,72 @@ function test_syncFinish_deleteLotsInBatches() {
}
}
function test_sync_rollback() {
_("SyncEngine.sync() rolls back tracker's changedIDs when syncing fails.");
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
Svc.Prefs.set("username", "foo");
// Set up a server without a "steam" collection handler, so sync will fail.
let crypto_steam = new ServerWBO('steam');
let server = sync_httpd_setup({
"/1.0/foo/storage/crypto/steam": crypto_steam.handler()
});
do_test_pending();
createAndUploadKeypair();
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
let engine = makeSteamEngine();
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
engine.lastSyncLocal = 456;
engine._store.items = {flying: "LNER Class A3 4472",
scotsman: "Flying Scotsman",
peppercorn: "Peppercorn Class"};
// Mark these records as changed
const FLYING_CHANGED = 12345;
const SCOTSMAN_CHANGED = 23456;
const PEPPERCORN_CHANGED = 34567;
engine._tracker.addChangedID('flying', FLYING_CHANGED);
engine._tracker.addChangedID('scotsman', SCOTSMAN_CHANGED);
engine._tracker.addChangedID('peppercorn', PEPPERCORN_CHANGED);
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = {steam: {version: engine.version,
syncID: engine.syncID}};
try {
// Confirm initial environment
do_check_eq(engine.lastSyncLocal, 456);
do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
engine.enabled = true;
let error;
try {
engine.sync();
} catch (ex) {
error = ex;
}
do_check_true(!!error);
// Verify that the tracker state and local timestamp has been rolled back.
do_check_eq(engine.lastSyncLocal, 456);
do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
} finally {
server.stop(do_test_finished);
Svc.Prefs.resetBranch("");
Records.clearCache();
CryptoMetas.clearCache();
syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
}
}
function test_canDecrypt_noCryptoMeta() {
_("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key.");
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
@ -1162,13 +1236,14 @@ function run_test() {
test_processIncoming_emptyServer();
test_processIncoming_createFromServer();
test_processIncoming_reconcile();
test_processIncoming_fetchNum();
test_processIncoming_mobile_batchSize();
test_uploadOutgoing_toEmptyServer();
test_uploadOutgoing_failed();
test_uploadOutgoing_MAX_UPLOAD_RECORDS();
test_syncFinish_noDelete();
test_syncFinish_deleteByIds();
test_syncFinish_deleteLotsInBatches();
test_sync_rollback();
test_canDecrypt_noCryptoMeta();
test_canDecrypt_true();
}

Просмотреть файл

@ -43,11 +43,12 @@ function fakeSvcSession() {
function run_test() {
let engine = new TabEngine();
engine._store.wipe();
_("Verify we have an empty tracker to work with");
_("We assume that tabs have changed at startup.");
let tracker = engine._tracker;
do_check_eq([id for (id in tracker.changedIDs)].length, 0);
do_check_true(tracker.modified);
do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
[Clients.localID]));
let logs;
@ -83,15 +84,28 @@ function run_test() {
logs = fakeSvcSession();
let idx = 0;
for each (let evttype in ["TabOpen", "TabClose", "TabSelect"]) {
// Pretend we just synced.
tracker.clearChangedIDs();
do_check_false(tracker.modified);
// Send a fake tab event
tracker.onTab({type: evttype , originalTarget: evttype});
do_check_eq([id for (id in tracker.changedIDs)].length, 1);
do_check_true(tracker.modified);
do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
[Clients.localID]));
do_check_eq(logs.length, idx+1);
do_check_eq(logs[idx].target, evttype);
do_check_eq(logs[idx].prop, "weaveLastUsed");
do_check_true(typeof logs[idx].value == "number");
idx++;
}
// Pretend we just synced.
tracker.clearChangedIDs();
do_check_false(tracker.modified);
tracker.onTab({type: "pageshow", originalTarget: "pageshow"});
do_check_eq([id for (id in tracker.changedIDs)].length, 1);
do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
[Clients.localID]));
do_check_eq(logs.length, idx); // test that setTabValue isn't called
}

Просмотреть файл

@ -0,0 +1,27 @@
Cu.import("resource://services-sync/util.js");
function run_test() {
_("Chain a few async methods, making sure the 'this' object is correct.");
let methods = {
save: function(x, callback) {
this.x = x;
callback(x);
},
addX: function(x, callback) {
callback(x + this.x);
},
double: function(x, callback) {
callback(x * 2);
},
neg: function(x, callback) {
callback(-x);
}
};
methods.chain = Utils.asyncChain;
// ((1 + 1 + 1) * (-1) + 1) * 2 + 1 = -3
methods.chain(methods.save, methods.addX, methods.addX, methods.neg,
methods.addX, methods.double, methods.addX, methods.save)(1);
do_check_eq(methods.x, -3);
}