Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This is a JavaScript module to be imported via
* ChromeUtils.importESModule() and acts as a singleton. Only the following
* listed symbols will exposed on import, and only when and where imported.
*/
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
Log: "resource://gre/modules/Log.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "fileProtocolHandler", () => {
let fileHandler = Services.io.getProtocolHandler("file");
return fileHandler.QueryInterface(Ci.nsIFileProtocolHandler);
});
ChromeUtils.defineLazyGetter(lazy, "gTextDecoder", () => {
return new TextDecoder();
});
// Options for wiping data during a sync
const SYNC_RESET_CLIENT = "resetClient";
const SYNC_WIPE_CLIENT = "wipeClient";
const SYNC_WIPE_REMOTE = "wipeRemote";
// Actions a test can perform
const ACTION_ADD = "add";
const ACTION_DELETE = "delete";
const ACTION_MODIFY = "modify";
const ACTION_SET_ENABLED = "set-enabled";
const ACTION_SYNC = "sync";
const ACTION_SYNC_RESET_CLIENT = SYNC_RESET_CLIENT;
const ACTION_SYNC_WIPE_CLIENT = SYNC_WIPE_CLIENT;
const ACTION_SYNC_WIPE_REMOTE = SYNC_WIPE_REMOTE;
const ACTION_VERIFY = "verify";
const ACTION_VERIFY_NOT = "verify-not";
const OBSERVER_TOPICS = [
"fxaccounts:onlogin",
"fxaccounts:onlogout",
"profile-before-change",
"weave:service:tracking-started",
"weave:service:tracking-stopped",
"weave:service:login:error",
"weave:service:setup-complete",
"weave:service:sync:finish",
"weave:service:sync:delayed",
"weave:service:sync:error",
"weave:service:sync:start",
"weave:service:resyncs-finished",
"places-browser-init-complete",
];
export var TPS = {
_currentAction: -1,
_currentPhase: -1,
_enabledEngines: null,
_errors: 0,
_isTracking: false,
_phaseFinished: false,
_phaselist: {},
_setupComplete: false,
_syncActive: false,
_syncCount: 0,
_syncsReportedViaTelemetry: 0,
_syncErrors: 0,
_syncWipeAction: null,
_tabsAdded: 0,
_tabsFinished: 0,
_test: null,
_triggeredSync: false,
_msSinceEpoch: 0,
_requestedQuit: false,
shouldValidateAddons: false,
shouldValidateBookmarks: false,
shouldValidatePasswords: false,
shouldValidateForms: false,
_placesInitDeferred: Promise.withResolvers(),
ACTIONS: [
ACTION_ADD,
ACTION_DELETE,
ACTION_MODIFY,
ACTION_SET_ENABLED,
ACTION_SYNC,
ACTION_SYNC_RESET_CLIENT,
ACTION_SYNC_WIPE_CLIENT,
ACTION_SYNC_WIPE_REMOTE,
ACTION_VERIFY,
ACTION_VERIFY_NOT,
],
_init: function TPS__init() {
this.delayAutoSync();
OBSERVER_TOPICS.forEach(function (aTopic) {
Services.obs.addObserver(this, aTopic, true);
}, this);
// Some engines bump their score during their sync, which then causes
// another sync immediately (notably, prefs and addons). We don't want
// this to happen, and there's no obvious preference to kill it - so
// we do this nasty hack to ensure the global score is always zero.
Services.prefs.addObserver("services.sync.globalScore", () => {
if (lazy.Weave.Service.scheduler.globalScore != 0) {
lazy.Weave.Service.scheduler.globalScore = 0;
}
});
},
DumpError(msg, exc = null) {
this._errors++;
let errInfo;
if (exc) {
errInfo = lazy.Log.exceptionStr(exc); // includes details and stack-trace.
} else {
// always write a stack even if no error passed.
errInfo = lazy.Log.stackTrace(new Error());
}
lazy.Logger.logError(`[phase ${this._currentPhase}] ${msg} - ${errInfo}`);
this.quit();
},
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
observe: function TPS__observe(subject, topic) {
try {
lazy.Logger.logInfo("----------event observed: " + topic);
switch (topic) {
case "profile-before-change":
OBSERVER_TOPICS.forEach(function (topic) {
Services.obs.removeObserver(this, topic);
}, this);
lazy.Logger.close();
break;
case "places-browser-init-complete":
this._placesInitDeferred.resolve();
break;
case "weave:service:setup-complete":
this._setupComplete = true;
if (this._syncWipeAction) {
lazy.Weave.Svc.PrefBranch.setStringPref(
"firstSync",
this._syncWipeAction
);
this._syncWipeAction = null;
}
break;
case "weave:service:sync:error":
this._syncActive = false;
this.delayAutoSync();
// If this is the first sync error, retry...
if (this._syncErrors === 0) {
lazy.Logger.logInfo("Sync error; retrying...");
this._syncErrors++;
lazy.CommonUtils.nextTick(() => {
this.RunNextTestAction().catch(err => {
this.DumpError("RunNextTestActionFailed", err);
});
});
} else {
this._triggeredSync = false;
this.DumpError("Sync error; aborting test");
}
break;
case "weave:service:resyncs-finished":
this._syncActive = false;
this._syncErrors = 0;
this._triggeredSync = false;
this.delayAutoSync();
break;
case "weave:service:sync:start":
// Ensure that the sync operation has been started by TPS
if (!this._triggeredSync) {
this.DumpError(
"Automatic sync got triggered, which is not allowed."
);
}
this._syncActive = true;
break;
case "weave:service:tracking-started":
this._isTracking = true;
break;
case "weave:service:tracking-stopped":
this._isTracking = false;
break;
case "fxaccounts:onlogin":
// A user signed in - for TPS that always means sync - so configure
// that.
lazy.Weave.Service.configure().catch(e => {
this.DumpError("Configuring sync failed.", e);
});
break;
default:
lazy.Logger.logInfo(`unhandled event: ${topic}`);
}
} catch (e) {
this.DumpError("Observer failed", e);
}
},
/**
* Given that we cannot completely disable the automatic sync operations, we
* massively delay the next sync. Sync operations have to only happen when
* directly called via TPS.Sync()!
*/
delayAutoSync: function TPS_delayAutoSync() {
lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200);
lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200);
lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200);
lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000);
},
quit: function TPS__quit() {
lazy.Logger.logInfo("quitting");
this._requestedQuit = true;
this.goQuitApplication();
},
async HandleWindows(aWindow, action) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on window " +
JSON.stringify(aWindow)
);
switch (action) {
case ACTION_ADD:
await lazy.BrowserWindows.Add(aWindow.private);
break;
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on windows"
);
},
async HandleTabs(tabs, action) {
for (let tab of tabs) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on tab " +
JSON.stringify(tab)
);
switch (action) {
case ACTION_ADD:
await lazy.BrowserTabs.Add(tab.uri);
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(
typeof tab.profile != "undefined",
"profile must be defined when verifying tabs"
);
lazy.Logger.AssertTrue(
await lazy.BrowserTabs.Find(tab.uri, tab.title, tab.profile),
"error locating tab"
);
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertTrue(
typeof tab.profile != "undefined",
"profile must be defined when verifying tabs"
);
lazy.Logger.AssertTrue(
await !lazy.BrowserTabs.Find(tab.uri, tab.title, tab.profile),
"tab found which was expected to be absent"
);
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on tabs"
);
},
async HandlePrefs(prefs, action) {
for (let pref of prefs) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on pref " +
JSON.stringify(pref)
);
let preference = new lazy.Preference(pref);
switch (action) {
case ACTION_MODIFY:
preference.Modify();
break;
case ACTION_VERIFY:
preference.Find();
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on pref"
);
},
async HandleForms(data, action) {
this.shouldValidateForms = true;
for (let datum of data) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on form entry " +
JSON.stringify(datum)
);
let formdata = new lazy.FormData(datum, this._msSinceEpoch);
switch (action) {
case ACTION_ADD:
await formdata.Create();
break;
case ACTION_DELETE:
await formdata.Remove();
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(await formdata.Find(), "form data not found");
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertTrue(
!(await formdata.Find()),
"form data found, but it shouldn't be present"
);
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on formdata"
);
},
async HandleHistory(entries, action) {
try {
for (let entry of entries) {
const entryString = JSON.stringify(entry);
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on history entry " +
entryString
);
switch (action) {
case ACTION_ADD:
await lazy.HistoryEntry.Add(entry, this._msSinceEpoch);
break;
case ACTION_DELETE:
await lazy.HistoryEntry.Delete(entry, this._msSinceEpoch);
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(
await lazy.HistoryEntry.Find(entry, this._msSinceEpoch),
"Uri visits not found in history database: " + entryString
);
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertTrue(
!(await lazy.HistoryEntry.Find(entry, this._msSinceEpoch)),
"Uri visits found in history database, but they shouldn't be: " +
entryString
);
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on history"
);
} catch (e) {
await lazy.DumpHistory();
throw e;
}
},
async HandlePasswords(passwords, action) {
this.shouldValidatePasswords = true;
try {
for (let password of passwords) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on password " +
JSON.stringify(password)
);
let passwordOb = new lazy.Password(password);
switch (action) {
case ACTION_ADD:
lazy.Logger.AssertTrue(
(await passwordOb.Create()) > -1,
"error adding password"
);
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(
(await passwordOb.Find()) != -1,
"password not found"
);
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertTrue(
(await passwordOb.Find()) == -1,
"password found, but it shouldn't exist"
);
break;
case ACTION_DELETE:
lazy.Logger.AssertTrue(
(await passwordOb.Find()) != -1,
"password not found"
);
passwordOb.Remove();
break;
case ACTION_MODIFY:
if (passwordOb.updateProps != null) {
lazy.Logger.AssertTrue(
(await passwordOb.Find()) != -1,
"password not found"
);
passwordOb.Update();
}
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on passwords"
);
} catch (e) {
await lazy.DumpPasswords();
throw e;
}
},
async HandleAddons(addons, action, state) {
this.shouldValidateAddons = true;
for (let entry of addons) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on addon " +
JSON.stringify(entry)
);
let addon = new lazy.Addon(this, entry);
switch (action) {
case ACTION_ADD:
await addon.install();
break;
case ACTION_DELETE:
await addon.uninstall();
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(
await addon.find(state),
"addon " + addon.id + " not found"
);
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertFalse(
await addon.find(state),
"addon " + addon.id + " is present, but it shouldn't be"
);
break;
case ACTION_SET_ENABLED:
lazy.Logger.AssertTrue(
await addon.setEnabled(state),
"addon " + addon.id + " not found"
);
break;
default:
throw new Error("Unknown action for add-on: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on addons"
);
},
async HandleBookmarks(bookmarks, action) {
// wait for default bookmarks to be created.
await this._placesInitDeferred.promise;
this.shouldValidateBookmarks = true;
try {
let items = [];
for (let folder in bookmarks) {
let last_item_pos = -1;
for (let bookmark of bookmarks[folder]) {
lazy.Logger.clearPotentialError();
let placesItem;
bookmark.location = folder;
if (last_item_pos != -1) {
bookmark.last_item_pos = last_item_pos;
}
let itemGuid = null;
if (action != ACTION_MODIFY && action != ACTION_DELETE) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on bookmark " +
JSON.stringify(bookmark)
);
}
if ("uri" in bookmark) {
placesItem = new lazy.Bookmark(bookmark);
} else if ("folder" in bookmark) {
placesItem = new lazy.BookmarkFolder(bookmark);
} else if ("livemark" in bookmark) {
placesItem = new lazy.Livemark(bookmark);
} else if ("separator" in bookmark) {
placesItem = new lazy.Separator(bookmark);
}
if (action == ACTION_ADD) {
itemGuid = await placesItem.Create();
} else {
itemGuid = await placesItem.Find();
if (action == ACTION_VERIFY_NOT) {
lazy.Logger.AssertTrue(
itemGuid == null,
"places item exists but it shouldn't: " +
JSON.stringify(bookmark)
);
} else {
lazy.Logger.AssertTrue(itemGuid, "places item not found", true);
}
}
last_item_pos = await placesItem.GetItemIndex();
items.push(placesItem);
}
}
if (action == ACTION_DELETE || action == ACTION_MODIFY) {
for (let item of items) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on bookmark " +
JSON.stringify(item)
);
switch (action) {
case ACTION_DELETE:
await item.Remove();
break;
case ACTION_MODIFY:
if (item.updateProps != null) {
await item.Update();
}
break;
}
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on bookmarks"
);
} catch (e) {
await lazy.DumpBookmarks();
throw e;
}
},
async HandleAddresses(addresses, action) {
try {
for (let address of addresses) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on address " +
JSON.stringify(address)
);
let addressOb = new lazy.Address(address);
switch (action) {
case ACTION_ADD:
await addressOb.Create();
break;
case ACTION_MODIFY:
await addressOb.Update();
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(await addressOb.Find(), "address not found");
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertTrue(
!(await addressOb.Find()),
"address found, but it shouldn't exist"
);
break;
case ACTION_DELETE:
lazy.Logger.AssertTrue(await addressOb.Find(), "address not found");
await addressOb.Remove();
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on addresses"
);
} catch (e) {
await lazy.DumpAddresses();
throw e;
}
},
async HandleCreditCards(creditCards, action) {
try {
for (let creditCard of creditCards) {
lazy.Logger.logInfo(
"executing action " +
action.toUpperCase() +
" on creditCard " +
JSON.stringify(creditCard)
);
let creditCardOb = new lazy.CreditCard(creditCard);
switch (action) {
case ACTION_ADD:
await creditCardOb.Create();
break;
case ACTION_MODIFY:
await creditCardOb.Update();
break;
case ACTION_VERIFY:
lazy.Logger.AssertTrue(
await creditCardOb.Find(),
"creditCard not found"
);
break;
case ACTION_VERIFY_NOT:
lazy.Logger.AssertTrue(
!(await creditCardOb.Find()),
"creditCard found, but it shouldn't exist"
);
break;
case ACTION_DELETE:
lazy.Logger.AssertTrue(
await creditCardOb.Find(),
"creditCard not found"
);
await creditCardOb.Remove();
break;
default:
lazy.Logger.AssertTrue(false, "invalid action: " + action);
}
}
lazy.Logger.logPass(
"executing action " + action.toUpperCase() + " on creditCards"
);
} catch (e) {
await lazy.DumpCreditCards();
throw e;
}
},
async Cleanup() {
try {
await this.WipeServer();
} catch (ex) {
lazy.Logger.logError(
"Failed to wipe server: " + lazy.Log.exceptionStr(ex)
);
}
try {
if (await lazy.Authentication.isLoggedIn()) {
// signout and wait for Sync to completely reset itself.
lazy.Logger.logInfo("signing out");
let waiter = this.promiseObserver("weave:service:start-over:finish");
await lazy.Authentication.signOut();
await waiter;
lazy.Logger.logInfo("signout complete");
}
await lazy.Authentication.deleteEmail(this.config.fx_account.username);
} catch (e) {
lazy.Logger.logError("Failed to sign out: " + lazy.Log.exceptionStr(e));
}
},
/**
* Use Sync's bookmark validation code to see if we've corrupted the tree.
*/
async ValidateBookmarks() {
let getServerBookmarkState = async () => {
let bookmarkEngine = lazy.Weave.Service.engineManager.get("bookmarks");
let collection = bookmarkEngine.itemSource();
let collectionKey =
bookmarkEngine.service.collectionKeys.keyForCollection(
bookmarkEngine.name
);
collection.full = true;
let items = [];
let resp = await collection.get();
for (let json of resp.obj) {
let record = new collection._recordObj();
record.deserialize(json);
await record.decrypt(collectionKey);
items.push(record.cleartext);
}
return items;
};
let serverRecordDumpStr;
try {
lazy.Logger.logInfo("About to perform bookmark validation");
let clientTree = await lazy.PlacesUtils.promiseBookmarksTree("", {
includeItemIds: true,
});
let serverRecords = await getServerBookmarkState();
// We can't wait until catch to stringify this, since at that point it will have cycles.
serverRecordDumpStr = JSON.stringify(serverRecords);
let validator = new lazy.BookmarkValidator();
let { problemData } = await validator.compareServerWithClient(
serverRecords,
clientTree
);
for (let { name, count } of problemData.getSummary()) {
// Exclude mobile showing up on the server hackily so that we don't
// report it every time, see bug 1273234 and 1274394 for more information.
if (
name === "serverUnexpected" &&
problemData.serverUnexpected.includes("mobile")
) {
--count;
}
if (count) {
// Log this out before we assert. This is useful in the context of TPS logs, since we
// can see the IDs in the test files.
lazy.Logger.logInfo(
`Validation problem: "${name}": ${JSON.stringify(
problemData[name]
)}`
);
}
lazy.Logger.AssertEqual(
count,
0,
`Bookmark validation error of type ${name}`
);
}
} catch (e) {
// Dump the client records (should always be doable)
lazy.DumpBookmarks();
// Dump the server records if gotten them already.
if (serverRecordDumpStr) {
lazy.Logger.logInfo(
"Server bookmark records:\n" + serverRecordDumpStr + "\n"
);
}
this.DumpError("Bookmark validation failed", e);
}
lazy.Logger.logInfo("Bookmark validation finished");
},
async ValidateCollection(engineName, ValidatorType) {
let serverRecordDumpStr;
let clientRecordDumpStr;
try {
lazy.Logger.logInfo(`About to perform validation for "${engineName}"`);
let engine = lazy.Weave.Service.engineManager.get(engineName);
let validator = new ValidatorType(engine);
let serverRecords = await validator.getServerItems(engine);
let clientRecords = await validator.getClientItems();
try {
// This substantially improves the logs for addons while not making a
// substantial difference for the other two
clientRecordDumpStr = JSON.stringify(
clientRecords.map(r => {
let res = validator.normalizeClientItem(r);
delete res.original; // Try and prevent cyclic references
return res;
})
);
} catch (e) {
// ignore the error, the dump string is just here to make debugging easier.
clientRecordDumpStr = "<Cyclic value>";
}
try {
serverRecordDumpStr = JSON.stringify(serverRecords);
} catch (e) {
// as above
serverRecordDumpStr = "<Cyclic value>";
}
let { problemData } = await validator.compareClientWithServer(
clientRecords,
serverRecords
);
for (let { name, count } of problemData.getSummary()) {
if (count) {
lazy.Logger.logInfo(
`Validation problem: "${name}": ${JSON.stringify(
problemData[name]
)}`
);
}
lazy.Logger.AssertEqual(
count,
0,
`Validation error for "${engineName}" of type "${name}"`
);
}
} catch (e) {
// Dump the client records if possible
if (clientRecordDumpStr) {
lazy.Logger.logInfo(
`Client state for ${engineName}:\n${clientRecordDumpStr}\n`
);
}
// Dump the server records if gotten them already.
if (serverRecordDumpStr) {
lazy.Logger.logInfo(
`Server state for ${engineName}:\n${serverRecordDumpStr}\n`
);
}
this.DumpError(`Validation failed for ${engineName}`, e);
}
lazy.Logger.logInfo(`Validation finished for ${engineName}`);
},
ValidatePasswords() {
return this.ValidateCollection("passwords", lazy.PasswordValidator);
},
ValidateForms() {
return this.ValidateCollection("forms", lazy.FormValidator);
},
ValidateAddons() {
return this.ValidateCollection("addons", lazy.AddonValidator);
},
async RunNextTestAction() {
lazy.Logger.logInfo("Running next test action");
try {
if (this._currentAction >= this._phaselist[this._currentPhase].length) {
// Run necessary validations and then finish up
lazy.Logger.logInfo("No more actions - running validations...");
if (this.shouldValidateBookmarks) {
await this.ValidateBookmarks();
}
if (this.shouldValidatePasswords) {
await this.ValidatePasswords();
}
if (this.shouldValidateForms) {
await this.ValidateForms();
}
if (this.shouldValidateAddons) {
await this.ValidateAddons();
}
// Force this early so that we run the validation and detect missing pings
// *before* we start shutting down, since if we do it after, the python
// code won't notice the failure.
lazy.SyncTelemetry.shutdown();
// we're all done
lazy.Logger.logInfo(
"test phase " +
this._currentPhase +
": " +
(this._errors ? "FAIL" : "PASS")
);
this._phaseFinished = true;
this.quit();
return;
}
this.seconds_since_epoch = Services.prefs.getIntPref(
"tps.seconds_since_epoch"
);
if (this.seconds_since_epoch) {
// Places dislikes it if we add visits in the future. We pretend the
// real time is 1 minute ago to avoid issues caused by places using a
// different clock than the one that set the seconds_since_epoch pref.
this._msSinceEpoch = (this.seconds_since_epoch - 60) * 1000;
} else {
this.DumpError("seconds-since-epoch not set");
return;
}
let phase = this._phaselist[this._currentPhase];
let action = phase[this._currentAction];
lazy.Logger.logInfo("starting action: " + action[0].name);
await action[0].apply(this, action.slice(1));
this._currentAction++;
} catch (e) {
if (lazy.Async.isShutdownException(e)) {
if (this._requestedQuit) {
lazy.Logger.logInfo("Sync aborted due to requested shutdown");
} else {
this.DumpError(
"Sync aborted due to shutdown, but we didn't request it"
);
}
} else {
this.DumpError("RunNextTestAction failed", e);
}
return;
}
await this.RunNextTestAction();
},
_getFileRelativeToSourceRoot(testFileURL, relativePath) {
let file = lazy.fileProtocolHandler.getFileFromURLSpec(testFileURL);
let root = file.parent.parent.parent.parent.parent; // <root>/services/sync/tests/tps/test_foo.js // <root>/services/sync/tests/tps // <root>/services/sync/tests // <root>/services/sync // <root>/services // <root>
root.appendRelativePath(relativePath);
root.normalize();
return root;
},
_pingValidator: null,
// Default ping validator that always says the ping passes. This should be
// overridden unless the `testing.tps.skipPingValidation` pref is true.
get pingValidator() {
return this._pingValidator
? this._pingValidator
: {
validate() {
lazy.Logger.logInfo(
"Not validating ping -- disabled by pref or failure to load schema"
);
return { valid: true, errors: [] };
},
};
},
// Attempt to load the sync_ping_schema.json and initialize `this.pingValidator`
// based on the source of the tps file. Assumes that it's at "../unit/sync_ping_schema.json"
// relative to the directory the tps test file (testFile) is contained in.
_tryLoadPingSchema(testFile) {
if (Services.prefs.getBoolPref("testing.tps.skipPingValidation", false)) {
return;
}
try {
let schemaFile = this._getFileRelativeToSourceRoot(
testFile,
"services/sync/tests/unit/sync_ping_schema.json"
);
let stream = Cc[
"@mozilla.org/network/file-input-stream;1"
].createInstance(Ci.nsIFileInputStream);
stream.init(
schemaFile,
lazy.FileUtils.MODE_RDONLY,
lazy.FileUtils.PERMS_FILE,
0
);
let bytes = lazy.NetUtil.readInputStream(stream, stream.available());
let schema = JSON.parse(lazy.gTextDecoder.decode(bytes));
lazy.Logger.logInfo("Successfully loaded schema");
this._pingValidator = new lazy.JsonSchema.Validator(schema);
} catch (e) {
this.DumpError(
`Failed to load ping schema relative to "${testFile}".`,
e
);
}
},
/**
* Runs a single test phase.
*
* This is the main entry point for each phase of a test. The TPS command
* line driver loads this module and calls into the function with the
* arguments from the command line.
*
* When a phase is executed, the file is loaded as JavaScript into the
* current object.
*
* The following keys in the options argument have meaning:
*
* - ignoreUnusedEngines If true, unused engines will be unloaded from
* Sync. This makes output easier to parse and is
* useful for debugging test failures.
*
* @param file
* String URI of the file to open.
* @param phase
* String name of the phase to run.
* @param logpath
* String path of the log file to write to.
* @param options
* Object defining addition run-time options.
*/
async RunTestPhase(file, phase, logpath, options) {
try {
let settings = options || {};
lazy.Logger.init(logpath);
lazy.Logger.logInfo("Sync version: " + lazy.WEAVE_VERSION);
lazy.Logger.logInfo("Firefox buildid: " + Services.appinfo.appBuildID);
lazy.Logger.logInfo("Firefox version: " + Services.appinfo.version);
lazy.Logger.logInfo(
"Firefox source revision: " +
(AppConstants.SOURCE_REVISION_URL || "unknown")
);
lazy.Logger.logInfo("Firefox platform: " + AppConstants.platform);
// do some sync housekeeping
if (lazy.Weave.Service.isLoggedIn) {
this.DumpError("Sync logged in on startup...profile may be dirty");
return;
}
// Wait for Sync service to become ready.
if (!lazy.Weave.Status.ready) {
this.waitForEvent("weave:service:ready");
}
await lazy.Weave.Service.promiseInitialized;
// We only want to do this if we modified the bookmarks this phase.
this.shouldValidateBookmarks = false;
// Always give Sync an extra tick to initialize. If we waited for the
// service:ready event, this is required to ensure all handlers have
// executed.
await lazy.Async.promiseYield();
await this._executeTestPhase(file, phase, settings);
} catch (e) {
this.DumpError("RunTestPhase failed", e);
}
},
/**
* Executes a single test phase.
*
* This is called by RunTestPhase() after the environment is validated.
*/
async _executeTestPhase(file, phase, settings) {
try {
this.config = JSON.parse(Services.prefs.getStringPref("tps.config"));
// parse the test file
Services.scriptloader.loadSubScript(file, this);
this._currentPhase = phase;
// cleanup phases are in the format `cleanup-${profileName}`.
if (this._currentPhase.startsWith("cleanup-")) {
let profileToClean = this._currentPhase.slice("cleanup-".length);
this.phases[this._currentPhase] = profileToClean;
this.Phase(this._currentPhase, [[this.Cleanup]]);
} else {
// Don't bother doing this for cleanup phases.
this._tryLoadPingSchema(file);
}
let this_phase = this._phaselist[this._currentPhase];
if (this_phase == undefined) {
this.DumpError("invalid phase " + this._currentPhase);
return;
}
if (this.phases[this._currentPhase] == undefined) {
this.DumpError("no profile defined for phase " + this._currentPhase);
return;
}
// If we have restricted the active engines, unregister engines we don't
// care about.
if (settings.ignoreUnusedEngines && Array.isArray(this._enabledEngines)) {
let names = {};
for (let name of this._enabledEngines) {
names[name] = true;
}
for (let engine of lazy.Weave.Service.engineManager.getEnabled()) {
if (!(engine.name in names)) {
lazy.Logger.logInfo("Unregistering unused engine: " + engine.name);
await lazy.Weave.Service.engineManager.unregister(engine);
}
}
}
lazy.Logger.logInfo("Starting phase " + this._currentPhase);
lazy.Logger.logInfo(
"setting client.name to " + this.phases[this._currentPhase]
);
lazy.Weave.Svc.PrefBranch.setStringPref(
"client.name",
this.phases[this._currentPhase]
);
this._interceptSyncTelemetry();
// start processing the test actions
this._currentAction = 0;
await lazy.SessionStore.promiseAllWindowsRestored;
await this.RunNextTestAction();
} catch (e) {
this.DumpError("_executeTestPhase failed", e);
}
},
/**
* Override sync telemetry functions so that we can detect errors generating
* the sync ping, and count how many pings we report.
*/
_interceptSyncTelemetry() {
let originalObserve = lazy.SyncTelemetry.observe;
let self = this;
lazy.SyncTelemetry.observe = function () {
try {
originalObserve.apply(this, arguments);
} catch (e) {
self.DumpError("Error when generating sync telemetry", e);
}
};
lazy.SyncTelemetry.submit = record => {
lazy.Logger.logInfo(
"Intercepted sync telemetry submission: " + JSON.stringify(record)
);
this._syncsReportedViaTelemetry +=
record.syncs.length + (record.discarded || 0);
if (record.discarded) {
if (record.syncs.length != lazy.SyncTelemetry.maxPayloadCount) {
this.DumpError(
"Syncs discarded from ping before maximum payload count reached"
);
}
}
// If this is the shutdown ping, check and see that the telemetry saw all the syncs.
if (record.why === "shutdown") {
// If we happen to sync outside of tps manually causing it, its not an
// error in the telemetry, so we only complain if we didn't see all of them.
if (this._syncsReportedViaTelemetry < this._syncCount) {
this.DumpError(
`Telemetry missed syncs: Saw ${this._syncsReportedViaTelemetry}, should have >= ${this._syncCount}.`
);
}
}
if (!record.syncs.length) {
// Note: we're overwriting submit, so this is called even for pings that
// may have no data (which wouldn't be submitted to telemetry and would
// fail validation).
return;
}
// Our ping may have some undefined values, which we rely on JSON stripping
// out as part of the ping submission - but our validator fails with them,
// so round-trip via JSON here to avoid that.
record = JSON.parse(JSON.stringify(record));
const result = this.pingValidator.validate(record);
if (!result.valid) {
// Note that we already logged the record.
this.DumpError(
"Sync ping validation failed with errors: " +
JSON.stringify(result.errors)
);
}
};
},
/**
* Register a single phase with the test harness.
*
* This is called when loading individual test files.
*
* @param phasename
* String name of the phase being loaded.
* @param fnlist
* Array of functions/actions to perform.
*/
Phase: function Test__Phase(phasename, fnlist) {
if (Object.keys(this._phaselist).length === 0) {
// This is the first phase we should force a log in
fnlist.unshift([this.Login]);
}
this._phaselist[phasename] = fnlist;
},
/**
* Restrict enabled Sync engines to a specified set.
*
* This can be called by a test to limit what engines are enabled. It is
* recommended to call it to reduce the overhead and log clutter for the
* test.
*
* The "clients" engine is special and is always enabled, so there is no
* need to specify it.
*
* @param names
* Array of Strings for engines to make active during the test.
*/
EnableEngines: function EnableEngines(names) {
if (!Array.isArray(names)) {
throw new Error(
"Argument to RestrictEngines() is not an array: " + typeof names
);
}
this._enabledEngines = names;
},
/**
* Returns a promise that resolves when a specific observer notification is
* resolved. This is similar to the various waitFor* functions, although is
* typically safer if you need to do some other work that may make the event
* fire.
*
* eg:
* doSomething(); // causes the event to be fired.
* await promiseObserver("something");
* is risky as the call to doSomething may trigger the event before the
* promiseObserver call is made. Contrast with:
*
* let waiter = promiseObserver("something");
* doSomething(); // causes the event to be fired.
* await waiter; // will return as soon as the event fires, even if it fires
* // before this function is called.
*
* @param aEventName
* String event to wait for.
*/
promiseObserver(aEventName) {
return new Promise(resolve => {
lazy.Logger.logInfo("Setting up wait for " + aEventName + "...");
let handler = () => {
lazy.Logger.logInfo("Observed " + aEventName);
lazy.Svc.Obs.remove(aEventName, handler);
resolve();
};
lazy.Svc.Obs.add(aEventName, handler);
});
},
/**
* Wait for the named event to be observed.
*
* Note that in general, you should probably use promiseObserver unless you
* are 100% sure that the event being waited on can only be sent after this
* call adds the listener.
*
* @param aEventName
* String event to wait for.
*/
async waitForEvent(aEventName) {
await this.promiseObserver(aEventName);
},
/**
* Waits for Sync to logged in before returning
*/
async waitForSetupComplete() {
if (!this._setupComplete) {
await this.waitForEvent("weave:service:setup-complete");
}
},
/**
* Waits for Sync to be finished before returning
*/
async waitForSyncFinished() {
if (lazy.Weave.Service.locked) {
await this.waitForEvent("weave:service:resyncs-finished");
}
},
/**
* Waits for Sync to start tracking before returning.
*/
async waitForTracking() {
if (!this._isTracking) {
await this.waitForEvent("weave:service:tracking-started");
}
},
/**
* Login on the server
*/
async Login() {
if (await lazy.Authentication.isReady()) {
return;
}
lazy.Logger.logInfo("Setting client credentials and login.");
await lazy.Authentication.signIn(this.config.fx_account);
await this.waitForSetupComplete();
lazy.Logger.AssertEqual(
lazy.Weave.Status.service,
lazy.STATUS_OK,
"Weave status OK"
);
await this.waitForTracking();
},
/**
* Triggers a sync operation
*
* @param {String} [wipeAction]
* Type of wipe to perform (resetClient, wipeClient, wipeRemote)
*
*/
async Sync(wipeAction) {
if (this._syncActive) {
this.DumpError("Sync currently active which should be impossible");
return;
}
lazy.Logger.logInfo(
"Executing Sync" + (wipeAction ? ": " + wipeAction : "")
);
// Force a wipe action if requested. In case of an initial sync the pref
// will be overwritten by Sync itself (see bug 992198), so ensure that we
// also handle it via the "weave:service:setup-complete" notification.
if (wipeAction) {
this._syncWipeAction = wipeAction;
lazy.Weave.Svc.PrefBranch.setStringPref("firstSync", wipeAction);
} else {
lazy.Weave.Svc.PrefBranch.clearUserPref("firstSync");
}
if (!(await lazy.Weave.Service.login())) {
// We need to complete verification.
lazy.Logger.logInfo("Logging in before performing sync");
await this.Login();
}
++this._syncCount;
lazy.Logger.logInfo(
"Executing Sync" + (wipeAction ? ": " + wipeAction : "")
);
this._triggeredSync = true;
await lazy.Weave.Service.sync();
lazy.Logger.logInfo("Sync is complete");
// wait a second for things to settle...
await new Promise(resolve => {
lazy.CommonUtils.namedTimer(resolve, 1000, this, "postsync");
});
},
async WipeServer() {
lazy.Logger.logInfo("Wiping data from server.");
await this.Login();
await lazy.Weave.Service.login();
await lazy.Weave.Service.wipeServer();
},
/**
* Action which ensures changes are being tracked before returning.
*/
async EnsureTracking() {
await this.Login();
await this.waitForTracking();
},
Addons: {
async install(addons) {
await TPS.HandleAddons(addons, ACTION_ADD);
},
async setEnabled(addons, state) {
await TPS.HandleAddons(addons, ACTION_SET_ENABLED, state);
},
async uninstall(addons) {
await TPS.HandleAddons(addons, ACTION_DELETE);
},
async verify(addons, state) {
await TPS.HandleAddons(addons, ACTION_VERIFY, state);
},
async verifyNot(addons) {
await TPS.HandleAddons(addons, ACTION_VERIFY_NOT);
},
skipValidation() {
TPS.shouldValidateAddons = false;
},
},
Addresses: {
async add(addresses) {
await this.HandleAddresses(addresses, ACTION_ADD);
},
async modify(addresses) {
await this.HandleAddresses(addresses, ACTION_MODIFY);
},
async delete(addresses) {
await this.HandleAddresses(addresses, ACTION_DELETE);
},
async verify(addresses) {
await this.HandleAddresses(addresses, ACTION_VERIFY);
},
async verifyNot(addresses) {
await this.HandleAddresses(addresses, ACTION_VERIFY_NOT);
},
},
Bookmarks: {
async add(bookmarks) {
await TPS.HandleBookmarks(bookmarks, ACTION_ADD);
},
async modify(bookmarks) {
await TPS.HandleBookmarks(bookmarks, ACTION_MODIFY);
},
async delete(bookmarks) {
await TPS.HandleBookmarks(bookmarks, ACTION_DELETE);
},
async verify(bookmarks) {
await TPS.HandleBookmarks(bookmarks, ACTION_VERIFY);
},
async verifyNot(bookmarks) {
await TPS.HandleBookmarks(bookmarks, ACTION_VERIFY_NOT);
},
skipValidation() {
TPS.shouldValidateBookmarks = false;
},
},
CreditCards: {
async add(creditCards) {
await this.HandleCreditCards(creditCards, ACTION_ADD);
},
async modify(creditCards) {
await this.HandleCreditCards(creditCards, ACTION_MODIFY);
},
async delete(creditCards) {
await this.HandleCreditCards(creditCards, ACTION_DELETE);
},
async verify(creditCards) {
await this.HandleCreditCards(creditCards, ACTION_VERIFY);
},
async verifyNot(creditCards) {
await this.HandleCreditCards(creditCards, ACTION_VERIFY_NOT);
},
},
Formdata: {
async add(formdata) {
await this.HandleForms(formdata, ACTION_ADD);
},
async delete(formdata) {
await this.HandleForms(formdata, ACTION_DELETE);
},
async verify(formdata) {
await this.HandleForms(formdata, ACTION_VERIFY);
},
async verifyNot(formdata) {
await this.HandleForms(formdata, ACTION_VERIFY_NOT);
},
},
History: {
async add(history) {
await this.HandleHistory(history, ACTION_ADD);
},
async delete(history) {
await this.HandleHistory(history, ACTION_DELETE);
},
async verify(history) {
await this.HandleHistory(history, ACTION_VERIFY);
},
async verifyNot(history) {
await this.HandleHistory(history, ACTION_VERIFY_NOT);
},
},
Passwords: {
async add(passwords) {
await this.HandlePasswords(passwords, ACTION_ADD);
},
async modify(passwords) {
await this.HandlePasswords(passwords, ACTION_MODIFY);
},
async delete(passwords) {
await this.HandlePasswords(passwords, ACTION_DELETE);
},
async verify(passwords) {
await this.HandlePasswords(passwords, ACTION_VERIFY);
},
async verifyNot(passwords) {
await this.HandlePasswords(passwords, ACTION_VERIFY_NOT);
},
skipValidation() {
TPS.shouldValidatePasswords = false;
},
},
Prefs: {
async modify(prefs) {
await TPS.HandlePrefs(prefs, ACTION_MODIFY);
},
async verify(prefs) {
await TPS.HandlePrefs(prefs, ACTION_VERIFY);
},
},
Tabs: {
async add(tabs) {
await TPS.HandleTabs(tabs, ACTION_ADD);
},
async verify(tabs) {
await TPS.HandleTabs(tabs, ACTION_VERIFY);
},
async verifyNot(tabs) {
await TPS.HandleTabs(tabs, ACTION_VERIFY_NOT);
},
},
Windows: {
async add(aWindow) {
await TPS.HandleWindows(aWindow, ACTION_ADD);
},
},
// Jumping through loads of hoops via calling back into a "HandleXXX" method
// and adding an ACTION_XXX indirection adds no value - let's KISS!
// eslint-disable-next-line no-unused-vars
ExtStorage: {
async set(id, data) {
lazy.Logger.logInfo(`setting data for '${id}': ${data}`);
await lazy.extensionStorageSync.set({ id }, data);
},
async verify(id, keys, data) {
let got = await lazy.extensionStorageSync.get({ id }, keys);
lazy.Logger.AssertEqual(got, data, `data for '${id}'/${keys}`);
},
},
};
// Initialize TPS
TPS._init();