--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -17,16 +17,20 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
DefaultWeakMap,
promiseEvent,
} = ExtensionUtils;
@@ -152,16 +156,17 @@ class BasePopup {
} else if (finalize) {
this.receiveMessage = () => {};
}
}
// Returns the name of the event fired on `viewNode` when the popup is being
// destroyed. This must be implemented by every subclass.
get DESTROY_EVENT() {
+ /* istanbul ignore next */
throw new Error("Not implemented");
}
get STYLESHEETS() {
let sheets = [];
if (this.browserStyle) {
sheets.push(...popupStylesheets);
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -93,16 +93,17 @@ BrowserAction.prototype = {
view.setAttribute("flex", "1");
document.getElementById("PanelUI-multiView").appendChild(view);
document.addEventListener("popupshowing", this);
},
onDestroyed: document => {
let view = document.getElementById(this.viewId);
+ /* istanbul ignore else */
if (view) {
this.clearPopup();
CustomizableUI.hidePanelForNode(view);
view.remove();
}
document.removeEventListener("popupshowing", this);
},
@@ -127,17 +128,19 @@ BrowserAction.prototype = {
// Popups are shown only if a popup URL is defined; otherwise
// a "click" event is dispatched. This is done for compatibility with the
// Google Chrome onClicked extension API.
if (popupURL) {
try {
let popup = this.getPopup(document.defaultView, popupURL);
event.detail.addBlocker(popup.attach(event.target));
} catch (e) {
+ /* istanbul ignore next */
Cu.reportError(e);
+ /* istanbul ignore next */
event.preventDefault();
}
} else {
// This isn't not a hack, but it seems to provide the correct behavior
// with the fewest complications.
event.preventDefault();
this.emit("click");
}
@@ -368,16 +371,17 @@ BrowserAction.prototype = {
--webextension-toolbar-image: url("${escape(icon)}");
--webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
`);
},
// Update the toolbar button for a given window.
updateWindow(window) {
let widget = this.widget.forWindow(window);
+ /* istanbul ignore else */
if (widget) {
let tab = window.gBrowser.selectedTab;
this.updateButton(widget.node, this.tabContext.get(tab));
}
},
// Update the toolbar button when the extension changes the icon,
// title, badge, etc. If it only changes a parameter for a single
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -418,29 +418,31 @@ MenuItem.prototype = {
this.root.addChild(this);
} else {
let menuMap = gContextMenuMap.get(this.extension);
menuMap.get(parentId).addChild(this);
}
},
get parentId() {
- return this.parent ? this.parent.id : undefined;
+ return this.parent ? this.parent.id : /* istanbul ignore next */ undefined;
},
addChild(child) {
+ /* istanbul ignore if */
if (child.parent) {
throw new Error("Child MenuItem already has a parent.");
}
this.children.push(child);
child.parent = this;
},
detachChild(child) {
let idx = this.children.indexOf(child);
+ /* istanbul ignore if */
if (idx < 0) {
throw new Error("Child MenuItem not found, it cannot be removed.");
}
this.children.splice(idx, 1);
child.parent = null;
},
get root() {
@@ -623,16 +625,17 @@ extensions.registerSchemaAPI("contextMen
let menuItem = gContextMenuMap.get(extension).get(id);
if (menuItem) {
menuItem.remove();
}
},
removeAll: function() {
let root = gRootItems.get(extension);
+ /* istanbul ignore else */
if (root) {
root.remove();
}
},
onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => {
let listener = (event, info, tab) => {
fire.async(info, tab);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -338,16 +338,17 @@ extensions.registerSchemaAPI("tabs", "ad
}).api(),
create(createProperties) {
return new Promise((resolve, reject) => {
let window = createProperties.windowId !== null ?
windowTracker.getWindow(createProperties.windowId, context) :
windowTracker.topWindow;
+ /* istanbul ignore if */
if (!window.gBrowser) {
let obs = (finishedWindow, topic, data) => {
if (finishedWindow != window) {
return;
}
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
resolve(window);
};
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -57,16 +57,17 @@ TabContext.prototype = {
return this.tabData.get(tab);
},
clear(tab) {
this.tabData.delete(tab);
},
handleEvent(event) {
+ /* istanbul ignore else */
if (event.type == "TabSelect") {
let tab = event.target;
this.emit("tab-select", tab);
this.emit("location-change", tab);
}
},
onStateChange(browser, webProgress, request, stateFlags, statusCode) {
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -24,16 +24,20 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
"resource://testing-common/TestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebExtCoverage",
+ "resource://gre/modules/WebExtCoverage.jsm");
+
+do_register_cleanup(() => WebExtCoverage.saveAllCoverage(false));
ExtensionTestUtils.init(this);
/**
* Creates a new HttpServer for testing, and begins listening on the
* specified port. Automatically shuts down the server when the test
* unit ends.
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -64,16 +64,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/ExtensionContent.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/ExtensionParent.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
var {
GlobalManager,
ParentAPIManager,
@@ -342,16 +345,17 @@ this.ExtensionData = class {
}.bind(this));
}
readJSON(path) {
return new Promise((resolve, reject) => {
let uri = this.rootURI.resolve(`./${path}`);
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+ /* istanbul ignore if */
if (!Components.isSuccessCode(status)) {
// Convert status code to a string
let e = Components.Exception("", status);
reject(new Error(`Error while loading '${uri}' (${e.name})`));
return;
}
try {
let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
@@ -520,16 +524,17 @@ this.ExtensionData = class {
// Map(gecko-locale-code -> locale-directory-name)
promiseLocales() {
if (!this._promiseLocales) {
this._promiseLocales = Task.spawn(function* () {
let locales = new Map();
let entries = yield this.readDirectory("_locales");
for (let file of entries) {
+ /* istanbul ignore else */
if (file.isDir) {
let locale = this.normalizeLocaleCode(file.name);
locales.set(locale, file.name);
}
}
this.localeData = new LocaleData({
defaultLocale: this.defaultLocale,
@@ -853,16 +858,17 @@ this.Extension = class extends Extension
return this.readManifest().then(() => {
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
started = true;
if (!this.hasShutdown) {
return this.initLocale();
}
}).then(() => {
+ /* istanbul ignore if */
if (this.errors.length) {
return Promise.reject({errors: this.errors});
}
if (this.hasShutdown) {
return;
}
@@ -873,17 +879,17 @@ this.Extension = class extends Extension
// and it is used to run code that needs to be executed before
// any of the "startup" listeners.
this.emit("startup", this);
Management.emit("startup", this);
return this.runManifest(this.manifest);
}).then(() => {
Management.emit("ready", this);
- }).catch(e => {
+ }).catch(/* istanbul ignore next */ e => {
dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
Cu.reportError(e);
if (started) {
ExtensionManagement.shutdownExtension(this.uuid);
}
this.cleanupGeneratedFile();
@@ -941,16 +947,17 @@ this.Extension = class extends Extension
MessageChannel.abortResponses({extensionId: this.id});
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
}
+ /* istanbul ignore next */
observe(subject, topic, data) {
if (topic === "xpcom-shutdown") {
this.cleanupGeneratedFile();
}
}
hasPermission(perm) {
let match = /^manifest:(.*)/.exec(perm);
--- a/toolkit/components/extensions/ExtensionAPI.jsm
+++ b/toolkit/components/extensions/ExtensionAPI.jsm
@@ -16,16 +16,20 @@ Cu.import("resource://gre/modules/XPCOMU
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
const global = this;
class ExtensionAPI {
constructor(extension) {
this.extension = extension;
}
destroy() {
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -33,16 +33,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
const {
DefaultMap,
LimitedSet,
SingletonEventManager,
SpreadArgs,
defineLazyGetter,
getInnerWindowID,
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -21,16 +21,19 @@ Cu.import("resource://gre/modules/XPCOMU
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
var {
EventEmitter,
ExtensionError,
SpreadArgs,
getConsole,
getInnerWindowID,
getUniqueId,
@@ -70,23 +73,25 @@ class BaseContext {
if (this.incognito == null) {
this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
}
MessageChannel.setupMessageManagers([this.messageManager]);
let onPageShow = event => {
+ /* istanbul ignore else */
if (!event || event.target === document) {
this.docShell = docShell;
this.contentWindow = contentWindow;
this.active = true;
}
};
let onPageHide = event => {
+ /* istanbul ignore else */
if (!event || event.target === document) {
// Put this off until the next tick.
Promise.resolve().then(() => {
this.docShell = null;
this.contentWindow = null;
this.active = false;
});
}
@@ -101,35 +106,41 @@ class BaseContext {
if (this.active) {
contentWindow.removeEventListener("pagehide", onPageHide, true);
contentWindow.removeEventListener("pageshow", onPageShow, true);
}
},
});
}
+ /* istanbul ignore next */
get cloneScope() {
+ /* istanbul ignore next */
throw new Error("Not implemented");
}
+ /* istanbul ignore next */
get principal() {
+ /* istanbul ignore next */
throw new Error("Not implemented");
}
runSafe(...args) {
+ /* istanbul ignore if */
if (this.unloaded) {
Cu.reportError("context.runSafe called after context unloaded");
} else if (!this.active) {
Cu.reportError("context.runSafe called while context is inactive");
} else {
return runSafeSync(this, ...args);
}
}
runSafeWithoutClone(...args) {
+ /* istanbul ignore if */
if (this.unloaded) {
Cu.reportError("context.runSafeWithoutClone called after context unloaded");
} else if (!this.active) {
Cu.reportError("context.runSafeWithoutClone called while context is inactive");
} else {
return runSafeSyncWithoutClone(...args);
}
}
@@ -192,17 +203,17 @@ class BaseContext {
* @param {object} data
* @param {object} [options]
* @param {object} [options.sender]
* @param {object} [options.recipient]
*
* @returns {Promise}
*/
sendMessage(target, messageName, data, options = {}) {
- options.recipient = options.recipient || {};
+ options.recipient = options.recipient || /* istanbul ignore next */ {};
options.sender = options.sender || {};
options.recipient.extensionId = this.extension.id;
options.sender.extensionId = this.extension.id;
options.sender.contextId = this.contextId;
return MessageChannel.sendMessage(target, messageName, data, options);
}
@@ -309,16 +320,17 @@ class BaseContext {
} else if (args instanceof SpreadArgs) {
runSafe(callback, ...args);
} else {
runSafe(callback, args);
}
},
error => {
this.withLastError(error, () => {
+ /* istanbul ignore if */
if (this.unloaded) {
dump(`Promise rejected after context unloaded\n`);
} else if (!this.active) {
dump(`Promise rejected while context is inactive\n`);
} else {
this.runSafeWithoutClone(callback);
}
});
@@ -566,16 +578,19 @@ class SchemaAPIManager extends EventEmit
* @returns {object} A sandbox that is used as the global by `loadScript`.
*/
_createExtGlobal() {
let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
wantXrays: false,
sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
});
+ global.__coverage__ = {};
+ WebExtCoverage.register(global);
+
Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
XPCOMUtils.defineLazyGetter(global, "console", getConsole);
XPCOMUtils.defineLazyModuleGetter(global, "require",
"resource://devtools/shared/Loader.jsm");
return global;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -42,16 +42,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
const {
EventEmitter,
LocaleData,
defineLazyGetter,
flushJarCache,
getInnerWindowID,
promiseDocumentReady,
@@ -342,16 +345,17 @@ class ContentScriptContextChild extends
// Make sure we don't hand out the system principal by accident.
// also make sure that the null principal has the right origin attributes
principal = ssm.createNullPrincipal(attrs);
} else {
principal = [contentPrincipal, extensionPrincipal];
}
if (isExtensionPage) {
+ /* istanbul ignore if */
if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
throw new Error("Invalid target window for this extension context");
}
// This is an iframe with content script API enabled and its principal should be the
// contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled
// because it enables us to create the APIs object in this sandbox object and then copying it
// into the iframe's window, see Bug 1214658 for rationale)
this.sandbox = Cu.Sandbox(contentWindow, {
@@ -574,16 +578,17 @@ DocumentManager = {
this.loadInto(window);
this.trigger("document_start", window);
}
/* eslint-disable mozilla/balanced-listeners */
window.addEventListener("DOMContentLoaded", this, true);
window.addEventListener("load", this, true);
/* eslint-enable mozilla/balanced-listeners */
+ /* istanbul ignore else */
} else if (topic == "inner-window-destroyed") {
let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
MessageChannel.abortResponses({innerWindowID: windowId});
// Close any existent content-script context for the destroyed window.
if (this.contentScriptWindows.has(windowId)) {
let extensions = this.contentScriptWindows.get(windowId);
@@ -602,16 +607,17 @@ DocumentManager = {
}
ExtensionChild.destroyExtensionContext(windowId);
}
},
handleEvent: function(event) {
let window = event.currentTarget;
+ /* istanbul ignore if */
if (event.target != window.document) {
// We use capturing listeners so we have precedence over content script
// listeners, but only care about events targeted to the element we're
// listening on.
return;
}
window.removeEventListener(event.type, this, true);
@@ -701,16 +707,17 @@ DocumentManager = {
return extensions.get(extension.id);
},
getExtensionPageContext(extension, window) {
let winId = getInnerWindowID(window);
let context = this.extensionPageWindows.get(winId);
+ /* istanbul ignore else */
if (!context) {
let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true});
this.extensionPageWindows.set(winId, context);
}
return context;
},
@@ -743,16 +750,17 @@ DocumentManager = {
if (context) {
context.close();
extensions.delete(extensionId);
}
}
// Clean up iframe extension page contexts on extension shutdown.
for (let [winId, context] of this.extensionPageWindows) {
+ /* istanbul ignore else */
if (context.extension.id == extensionId) {
context.close();
this.extensionPageWindows.delete(winId);
}
}
ExtensionChild.shutdownExtension(extensionId);
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -9,16 +9,19 @@ this.EXPORTED_SYMBOLS = ["ExtensionManag
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
@@ -193,16 +196,17 @@ var Service = {
extensionURILoadableByAnyone(uri) {
let uuid = uri.host;
let extension = this.uuidMap.get(uuid);
if (!extension || !extension.webAccessibleResources) {
return false;
}
let path = uri.QueryInterface(Ci.nsIURL).filePath;
+ /* istanbul ignore else */
if (path.length > 0 && path[0] == "/") {
path = path.substr(1);
}
return extension.webAccessibleResources.matches(path);
},
// Checks whether a given extension can load this URI (typically via
// an XML HTTP request). The manifest.json |permissions| directive
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -33,16 +33,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
var {
BaseContext,
SchemaAPIManager,
} = ExtensionCommon;
var {
MessageManagerProxy,
--- a/toolkit/components/extensions/ExtensionStorage.jsm
+++ b/toolkit/components/extensions/ExtensionStorage.jsm
@@ -8,21 +8,26 @@ this.EXPORTED_SYMBOLS = ["ExtensionStora
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
/**
* Helper function used to sanitize the objects that have to be saved in the ExtensionStorage.
*
* @param {BaseContext} context
* The current extension context.
* @param {string} key
* The key of the current JSON property.
* @param {any} value
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -13,35 +13,32 @@ this.EXPORTED_SYMBOLS = ["ExtensionStora
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
const global = this;
Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
const KINTO_PROD_SERVER_URL = "https://webextensions.settings.services.mozilla.com/v1";
const KINTO_DEFAULT_SERVER_URL = KINTO_PROD_SERVER_URL;
const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled";
const STORAGE_SYNC_SERVER_URL_PREF = "webextensions.storage.sync.serverURL";
const STORAGE_SYNC_SCOPE = "sync:addon_storage";
const STORAGE_SYNC_CRYPTO_COLLECTION_NAME = "storage-sync-crypto";
const STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID = "keys";
const FXA_OAUTH_OPTIONS = {
scope: STORAGE_SYNC_SCOPE,
};
// Default is 5sec, which seems a bit aggressive on the open internet
const KINTO_REQUEST_TIMEOUT = 30000;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-const {
- runSafeSyncWithoutClone,
-} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
-
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
"resource://services-sync/record.js");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
"resource://services-crypto/utils.js");
@@ -62,22 +59,32 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "Observers",
"resource://services-common/observers.js");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "KeyRingEncryptionRemoteTransformer",
"resource://services-sync/engines/extension-storage.js");
+
XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
STORAGE_SYNC_ENABLED_PREF, true);
XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
STORAGE_SYNC_SERVER_URL_PREF,
KINTO_DEFAULT_SERVER_URL);
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
+const {
+ runSafeSyncWithoutClone,
+} = ExtensionUtils;
+
/* globals prefPermitsStorageSync, prefStorageSyncServerURL */
// Map of Extensions to Set<Contexts> to track contexts that are still
// "live" and use storage.sync.
const extensionContexts = new Map();
// Borrow logger from Sync.
const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -14,16 +14,20 @@ var EXPORTED_SYMBOLS = ["TabTrackerBase"
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
const {
DefaultMap,
DefaultWeakMap,
EventEmitter,
ExtensionError,
} = ExtensionUtils;
--- a/toolkit/components/extensions/ExtensionTestCommon.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -31,16 +31,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyGetter(this, "apiManager",
() => ExtensionParent.apiManager);
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const {
flushJarCache,
instanceOf,
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -10,16 +10,19 @@ const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
const INTEGER = /^[1-9]\d*$/;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
@@ -34,16 +37,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
"@mozilla.org/content/style-sheet-service;1",
"nsIStyleSheetService");
+/* istanbul ignore next */
function getConsole() {
return new ConsoleAPI({
maxLogLevelPref: "extensions.webextensions.log.level",
prefix: "WebExtensions",
});
}
XPCOMUtils.defineLazyGetter(this, "console", getConsole);
@@ -56,68 +60,78 @@ function getUniqueId() {
}
/**
* An Error subclass for which complete error messages are always passed
* to extensions, rather than being interpreted as an unknown error.
*/
class ExtensionError extends Error {}
+/* istanbul ignore next */
function filterStack(error) {
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
}
// Run a function and report exceptions.
function runSafeSyncWithoutClone(f, ...args) {
try {
return f(...args);
} catch (e) {
+ /* istanbul ignore next */
dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
+ /* istanbul ignore next */
Cu.reportError(e);
}
}
// Run a function and report exceptions.
function runSafeWithoutClone(f, ...args) {
+ /* istanbul ignore if */
if (typeof(f) != "function") {
dump(`Extension error: expected function\n${filterStack(Error())}`);
return;
}
Promise.resolve().then(() => {
runSafeSyncWithoutClone(f, ...args);
});
}
// Run a function, cloning arguments into context.cloneScope, and
// report exceptions. |f| is expected to be in context.cloneScope.
function runSafeSync(context, f, ...args) {
+ /* istanbul ignore if */
if (context.unloaded) {
Cu.reportError("runSafeSync called after context unloaded");
return;
}
try {
args = Cu.cloneInto(args, context.cloneScope);
} catch (e) {
+ /* istanbul ignore next */
Cu.reportError(e);
+ /* istanbul ignore next */
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
}
return runSafeSyncWithoutClone(f, ...args);
}
// Run a function, cloning arguments into context.cloneScope, and
// report exceptions. |f| is expected to be in context.cloneScope.
function runSafe(context, f, ...args) {
try {
args = Cu.cloneInto(args, context.cloneScope);
} catch (e) {
+ /* istanbul ignore next */
Cu.reportError(e);
+ /* istanbul ignore next */
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
}
+ /* istanbul ignore if */
if (context.unloaded) {
dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
return undefined;
}
return runSafeWithoutClone(f, ...args);
}
function getInnerWindowID(window) {
@@ -129,16 +143,17 @@ function getInnerWindowID(window) {
// Return true if the given value is an instance of the given
// native type.
function instanceOf(value, type) {
return {}.toString.call(value) == `[object ${type}]`;
}
// Extend the object |obj| with the property descriptors of each object in
// |args|.
+/* istanbul ignore next */
function extend(obj, ...args) {
for (let arg of args) {
let props = [...Object.getOwnPropertyNames(arg),
...Object.getOwnPropertySymbols(arg)];
for (let prop of props) {
let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
Object.defineProperty(obj, prop, descriptor);
}
@@ -250,16 +265,17 @@ let IconDetails = {
} catch (e) {
// Function is called from extension code, delegate error.
if (context) {
throw e;
}
// If there's no context, it's because we're handling this
// as a manifest directive. Log a warning rather than
// raising an error.
+ /* istanbul ignore next */
extension.manifestError(`Invalid icon data: ${e}`);
}
return result;
},
// Returns the appropriate icon URL for the given icons object and the
// screen resolution of the given window.
@@ -350,16 +366,17 @@ class EventEmitter {
* Removes the given function as a listener for the given event.
*
* @param {string} event
* The name of the event to stop listening for.
* @param {function(string, ...any)} listener
* The listener function to remove.
*/
off(event, listener) {
+ /* istanbul ignore else */
if (this[LISTENERS].has(event)) {
let set = this[LISTENERS].get(event);
set.delete(listener);
if (!set.size) {
this[LISTENERS].delete(event);
}
}
@@ -386,17 +403,17 @@ class EventEmitter {
return Promise.all(promises);
}
}
function LocaleData(data) {
this.defaultLocale = data.defaultLocale;
this.selectedLocale = data.selectedLocale;
- this.locales = data.locales || new Map();
+ this.locales = data.locales || /* istanbul ignore next */ new Map();
this.warnedMissingKeys = new Set();
// Map(locale-name -> Map(message-key -> localized-string))
//
// Contains a key for each loaded locale, each of which is a
// Map of message keys to their localized strings.
this.messages = data.messages || new Map();
@@ -479,16 +496,17 @@ LocaleData.prototype = {
return rtl ? "right" : "left";
} else if (message == "@@bidi_end_edge") {
return rtl ? "left" : "right";
}
}
if (!this.warnedMissingKeys.has(message)) {
let error = `Unknown localization message ${message}`;
+ /* istanbul ignore else */
if (options.cloneScope) {
error = new options.cloneScope.Error(error);
}
Cu.reportError(error);
this.warnedMissingKeys.add(message);
}
return options.defaultValue;
},
@@ -520,24 +538,26 @@ LocaleData.prototype = {
// replacements. Later, it processes the resulting string for
// |$[0-9]| replacements.
//
// Again, it does not document this, but it accepts any number
// of sequential |$|s, and replaces them with that number minus
// 1. It also accepts |$| followed by any number of sequential
// digits, but refuses to process a localized string which
// provides more than 9 substitutions.
+ /* istanbul ignore if */
if (!instanceOf(messages, "Object")) {
extension.packagingError(`Invalid locale data for ${locale}`);
return result;
}
for (let key of Object.keys(messages)) {
let msg = messages[key];
+ /* istanbul ignore if */
if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
continue;
}
// Substitutions are case-insensitive, so normalize all of their names
// to lower-case.
let placeholders = new Map();
@@ -612,16 +632,17 @@ SingletonEventManager.prototype = {
return;
}
let shouldFire = () => {
if (this.context.unloaded) {
dump(`${this.name} event fired after context unloaded.\n`);
} else if (!this.context.active) {
dump(`${this.name} event fired while context is inactive.\n`);
+ /* istanbul ignore else */
} else if (this.unregister.has(callback)) {
return true;
}
return false;
};
let fire = {
sync: (...args) => {
@@ -653,16 +674,17 @@ SingletonEventManager.prototype = {
let unregister = this.register(fire, ...args);
this.unregister.set(callback, unregister);
this.context.callOnClose(this);
},
removeListener(callback) {
+ /* istanbul ignore if */
if (!this.unregister.has(callback)) {
return;
}
let unregister = this.unregister.get(callback);
this.unregister.delete(callback);
unregister();
if (this.unregister.size == 0) {
@@ -700,25 +722,28 @@ function ignoreEvent(context, name) {
.createInstance(Ci.nsIScriptError);
scriptError.init(msg, frame.filename, null, frame.lineNumber,
frame.columnNumber, Ci.nsIScriptError.warningFlag,
"content javascript");
let consoleService = Cc["@mozilla.org/consoleservice;1"]
.getService(Ci.nsIConsoleService);
consoleService.logMessage(scriptError);
},
+ /* istanbul ignore next */
removeListener: function(callback) {},
+ /* istanbul ignore next */
hasListener: function(callback) {},
};
}
// Copy an API object from |source| into the scope |dest|.
function injectAPI(source, dest) {
for (let prop in source) {
// Skip names prefixed with '_'.
+ /* istanbul ignore if */
if (prop[0] == "_") {
continue;
}
let desc = Object.getOwnPropertyDescriptor(source, prop);
if (typeof(desc.value) == "function") {
Cu.exportFunction(desc.value, dest, {defineAs: prop});
} else if (typeof(desc.value) == "object") {
@@ -766,32 +791,34 @@ class LimitedSet extends Set {
*/
function promiseDocumentReady(doc) {
if (doc.readyState == "interactive" || doc.readyState == "complete") {
return Promise.resolve(doc);
}
return new Promise(resolve => {
doc.addEventListener("DOMContentLoaded", function onReady(event) {
+ /* istanbul ignore else */
if (event.target === event.currentTarget) {
doc.removeEventListener("DOMContentLoaded", onReady, true);
resolve(doc);
}
}, true);
});
}
/**
* Returns a Promise which resolves when the given document is fully
* loaded.
*
* @param {Document} doc The document to await the load of.
* @returns {Promise<Document>}
*/
function promiseDocumentLoaded(doc) {
+ /* istanbul ignore if */
if (doc.readyState == "complete") {
return Promise.resolve(doc);
}
return new Promise(resolve => {
doc.defaultView.addEventListener("load", function(event) {
resolve(doc);
}, {once: true});
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -14,16 +14,18 @@ Components.utils.import("resource://gre/
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebExtCoverage",
+ "resource://gre/modules/WebExtCoverage.jsm");
XPCOMUtils.defineLazyGetter(this, "Management", () => {
const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
return Management;
});
/* exported ExtensionTestUtils */
@@ -232,16 +234,18 @@ var ExtensionTestUtils = {
currentScope: null,
profileDir: null,
init(scope) {
this.currentScope = scope;
+ scope.do_register_cleanup(() => WebExtCoverage.saveAllCoverage(false));
+
this.profileDir = scope.do_get_profile();
// We need to load at least one frame script into every message
// manager to ensure that the scriptable wrapper for its global gets
// created before we try to access it externally. If we don't, we
// fail sanity checks on debug builds the first time we try to
// create a wrapper, because we should never have a global without a
// cached wrapper.
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -19,16 +19,19 @@ Cu.import("resource://gre/modules/XPCOMU
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
var {
BaseContext,
} = ExtensionCommon;
var {
Messenger,
} = ExtensionChild;
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -1,14 +1,18 @@
/* 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/. */
"use strict";
+Components.utils.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
/**
* This module provides wrappers around standard message managers to
* simplify bidirectional communication. It currently allows a caller to
* send a message to a single listener, and receive a reply. If there
* are no matching listeners, or the message manager disconnects before
* a reply is received, the caller is returned an error.
*
* The listener end may specify filters for the messages it wishes to
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -5,16 +5,19 @@
"use strict";
this.EXPORTED_SYMBOLS = ["HostManifestManager", "NativeApp"];
/* globals NativeApp */
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -13,16 +13,20 @@ const global = this;
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
var {
DefaultMap,
instanceOf,
} = ExtensionUtils;
class DeepMap extends DefaultMap {
constructor() {
super(() => new DeepMap());
@@ -39,16 +43,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
this.EXPORTED_SYMBOLS = ["Schemas"];
/* globals Schemas, URL */
function readJSON(url) {
return new Promise((resolve, reject) => {
NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+ /* istanbul ignore if */
if (!Components.isSuccessCode(status)) {
// Convert status code to a string
let e = Components.Exception("", status);
reject(new Error(`Error while loading '${url}' (${e.name})`));
return;
}
try {
let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
@@ -57,16 +62,17 @@ function readJSON(url) {
// strip off for this to be valid JSON. As a hack, we just
// look for the first '[' character, which signals the start
// of the JSON content.
let index = text.indexOf("[");
text = text.slice(index);
resolve(JSON.parse(text));
} catch (e) {
+ /* istanbul ignore next */
reject(e);
}
});
});
}
/**
* Defines a lazy getter for the given property on the given object. Any
@@ -610,16 +616,17 @@ class Entry {
logDeprecation(context, value = null) {
let message = "This property is deprecated";
if (typeof(this.deprecated) == "string") {
message = this.deprecated;
if (message.includes("${value}")) {
try {
value = JSON.stringify(value);
} catch (e) {
+ /* istanbul ignore next */
value = String(value);
}
message = message.replace(/\$\{value\}/g, () => value);
}
}
context.logError(context.makeError(message));
}
@@ -718,25 +725,27 @@ class Type extends Entry {
}
}
// Takes a value, checks that it has the correct type, and returns a
// "normalized" version of the value. The normalized version will
// include "nulls" in place of omitted optional properties. The
// result of this function is either {error: "Some type error"} or
// {value: <normalized-value>}.
+ /* istanbul ignore next */
normalize(value, context) {
return context.error("invalid type");
}
// Unlike normalize, this function does a shallow check to see if
// |baseType| (one of the possible getValueBaseType results) is
// valid for this type. It returns true or false. It's used to fill
// in optional arguments to functions before actually type checking
-
+ // the arguments.
+ /* istanbul ignore next */
checkBaseType(baseType) {
return false;
}
// Helper method that simply relies on checkBaseType to implement
// normalize. Subclasses can choose to use it or not.
normalizeBase(type, value, context) {
if (this.checkBaseType(getValueBaseType(value))) {
@@ -850,16 +859,17 @@ class RefType extends Type {
super(schema);
this.namespaceName = namespaceName;
this.reference = reference;
}
get targetType() {
let ns = Schemas.namespaces.get(this.namespaceName);
let type = ns.get(this.reference);
+ /* istanbul ignore if */
if (!type) {
throw new Error(`Internal error: Type ${this.reference} not found`);
}
return type;
}
normalize(value, context) {
this.checkDeprecated(context, value);
@@ -1053,16 +1063,17 @@ class ObjectType extends Type {
this.properties = properties;
this.additionalProperties = additionalProperties;
this.patternProperties = patternProperties;
this.isInstanceOf = isInstanceOf;
}
extend(type) {
for (let key of Object.keys(type.properties)) {
+ /* istanbul ignore if */
if (key in this.properties) {
throw new Error(`InternalError: Attempt to extend an object with conflicting property "${key}"`);
}
this.properties[key] = type.properties[key];
}
this.patternProperties.push(...type.patternProperties);
@@ -1849,18 +1860,20 @@ this.Schemas = {
extendType(namespaceName, type) {
let ns = Schemas.namespaces.get(namespaceName);
let targetType = ns && ns.get(type.$extend);
// Only allow extending object and choices types for now.
if (targetType instanceof ObjectType) {
type.type = "object";
+ /* istanbul ignore if */
} else if (!targetType) {
throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`);
+ /* istanbul ignore if */
} else if (!(targetType instanceof ChoiceType)) {
throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`);
}
let parsed = this.parseSchema(type, [namespaceName], ["$extend"]);
if (parsed.constructor !== targetType.constructor) {
throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
}
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -2,18 +2,21 @@
* 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/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+WebExtCoverage.register(this);
+
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "require",
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -144,23 +144,25 @@ function checkSetCookiePermissions(exten
cookie.host = cookie.host.toLowerCase();
if (cookie.host != uri.host) {
// Not an exact match, so check for a valid subdomain.
let baseDomain;
try {
baseDomain = Services.eTLD.getBaseDomain(uri);
} catch (e) {
+ /* istanbul ignore else */
if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
// The cookie service uses these to determine whether the domain
// requires an exact match. We already know we don't have an exact
// match, so return false. In all other cases, re-raise the error.
return false;
}
+ /* istanbul ignore next */
throw e;
}
// The cookie domain must be a subdomain of the base domain. This prevents
// us from setting cookies for domains like ".co.uk".
// The domain of the requesting URL must likewise be a subdomain of the
// cookie domain. This prevents us from setting cookies for entirely
// unrelated domains.
new file mode 100755
--- /dev/null
+++ b/toolkit/components/extensions/instrument_code.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+from __future__ import print_function
+import json
+import os
+import subprocess
+
+
+EXTS = '.js', '.jsm'
+
+ROOT = 'dist/bin'
+
+DATA_DIRS = ('dist/bin', 'dist/bin/browser')
+
+PATTERNS = ('%s/dist/bin/%s/%s',
+ '%s/dist/Nightly.app/Contents/Resources/%s/%s')
+
+CODE_DIRS = ('toolkit/components/extensions/',
+ 'browser/components/extensions/',
+ 'toolkit/modules/addons/',
+ 'toolkit/components/utils/simpleServices.js')
+
+REPO = os.path.abspath('.')
+CODE_DIRS = tuple(os.path.join(REPO, d) for d in CODE_DIRS)
+
+processes = {}
+
+
+mach = subprocess.Popen(['./mach', 'environment', '--format=json'],
+ stdout=subprocess.PIPE)
+
+data = mach.communicate()[0]
+if not isinstance(data, type(u'')):
+ # Oh, Python...
+ data = data.decode('utf-8')
+
+config = json.loads(data)
+
+print('Entering object directory %s' % config['topobjdir'])
+objdir = config['topobjdir']
+
+
+def instrument(input, output):
+ print('Instrumenting %s' % input[len(REPO) + 1:])
+
+ os.unlink(output)
+ proc = subprocess.Popen(
+ ['babel',
+ '--plugins', 'transform-async-to-generator,istanbul',
+ '-o', output, input],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ processes[output] = proc
+
+for data_dir in DATA_DIRS:
+ data_file = '%s/faster/install_%s' % (objdir, data_dir.replace('/', '_'))
+ with open(data_file) as f:
+ for line in f:
+ fields = line.rstrip().split('\x1f')
+ if len(fields) == 3:
+ _, output, source = fields
+ if source.startswith(CODE_DIRS) and source.endswith(EXTS):
+ dir_ = data_dir[len(ROOT):]
+ for pat in PATTERNS:
+ file_ = pat % (objdir, dir_, output)
+ if os.path.exists(file_):
+ instrument(source, file_)
+
+for path, proc in processes.items():
+ stdout, stderr = proc.communicate()
+ if not stderr:
+ pass
+ else:
+ print('Error processing "%s": %s' % (path, stderr))
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
@@ -14,22 +14,24 @@
if (AppConstants.platform === "android") {
SimpleTest.requestLongerTimeout(6);
}
let windowData, testWindow;
add_task(function* setup() {
- let chromeScript = SpecialPowers.loadChromeScript(function() {
- let cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
- .getService(Components.interfaces.nsICacheStorageService);
- cache.clear();
- });
- chromeScript.destroy();
+ if (AppConstants.platform === "android") {
+ let chromeScript = SpecialPowers.loadChromeScript(function() {
+ let cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
+ .getService(Components.interfaces.nsICacheStorageService);
+ cache.clear();
+ });
+ chromeScript.destroy();
+ }
testWindow = window.open("about:blank", "_blank", "width=100,height=100");
yield waitForLoad(testWindow);
// Fetch the windowId and tabId we need to filter with WebRequest.
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: [
--- a/toolkit/components/extensions/test/xpcshell/head.js
+++ b/toolkit/components/extensions/test/xpcshell/head.js
@@ -24,16 +24,20 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebExtCoverage",
+ "resource://gre/modules/WebExtCoverage.jsm");
+
+do_register_cleanup(() => WebExtCoverage.saveAllCoverage(false));
ExtensionTestUtils.init(this);
/**
* Creates a new HttpServer for testing, and begins listening on the
* specified port. Automatically shuts down the server when the test
* unit ends.
*
new file mode 100755
--- /dev/null
+++ b/toolkit/components/extensions/test_coverage.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+set -e
+
+IFS="$(echo)"
+cd $(hg root)
+
+echo Instrumenting WebExtension code
+${PYTHON:-python} toolkit/components/extensions/instrument_code.py
+
+: ${MACH:=./mach}
+
+if test -n "$GECKO_JS_COVERAGE_OUTPUT_DIR"
+then tmpdir="$GECKO_JS_COVERAGE_OUTPUT_DIR"
+else tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/webext-coverage.XXXXXX")
+fi
+export GECKO_JS_COVERAGE_OUTPUT_DIR="$tmpdir"
+
+echo
+echo Outputting coverage data to: $tmpdir
+echo
+
+mochi() {
+ ${MACH} mochitest --keep-open=false "$@"
+ ${MACH} mochitest --keep-open=false --disable-e10s "$@"
+}
+
+mochi --quiet toolkit/components/extensions/test/mochitest
+mochi --quiet browser/components/extensions/test/browser
+mochi --quiet --tag webextensions \
+ devtools/client/aboutdebugging/test
+${MACH} xpcshell-test --tag webextensions \
+ toolkit/components/extensions/test/xpcshell \
+ browser/components/extensions/test/xpcshell \
+ toolkit/mozapps/extensions/test/xpcshell
+
+
+cd "$tmpdir"
+mkdir coverage
+
+echo
+echo Generating full coverage report at "$tmpdir/coverage/index.html"
+istanbul report html
+echo
+
+for dir in content default
+do
+ echo Generating $dir process coverage report at "$tmpdir/coverage/$dir/index.html"
+ istanbul report --dir "coverage/$dir" --include "coverage-$dir-*.json" html
+ echo
+done
+
+# vim:se sts=2 sw=2 et ft=sh:
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -13,16 +13,19 @@
"use strict";
const Cc = Components.classes;
const Cu = Components.utils;
const Ci = Components.interfaces;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
function AddonPolicyService() {
this.wrappedJSObject = this;
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -9,16 +9,20 @@ const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"];
/* globals MatchPattern, MatchGlobs */
const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "data"];
const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
// This function converts a glob pattern (containing * and possibly ?
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/addons/WebExtCoverage.jsm
@@ -0,0 +1,149 @@
+/* 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/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+this.EXPORTED_SYMBOLS = ["WebExtCoverage"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "environment",
+ "@mozilla.org/process/environment;1",
+ Ci.nsIEnvironment);
+
+
+const COVERAGE_PROPERTY = "__coverage__";
+const ENV_COVERAGE_OUTPUT_DIR = "GECKO_JS_COVERAGE_OUTPUT_DIR";
+
+const COVERAGE_MESSAGE = "WebExtCoverage:Update";
+
+const PROCESS_TYPES = Object.freeze({
+ [Services.appinfo.PROCESS_TYPE_DEFAULT]: "default",
+ [Services.appinfo.PROCESS_TYPE_CONTENT]: "content",
+});
+
+
+let globalID = 0;
+let initialized = false;
+
+const isContent = Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT;
+
+function mangleFilename(filename) {
+ return filename.replace(new RegExp("^(resource|chrome)://[^/]+/|^file://.*/obj[^/]*/dist/bin/"), "")
+ .replace(/\//g, "-");
+}
+
+this.WebExtCoverage = {
+ promises: new Set(),
+
+ coverageObjects: new Map(),
+
+ init() {
+ if (initialized) {
+ return this.outputDir;
+ }
+ initialized = true;
+
+ this.outputDir = environment.get(ENV_COVERAGE_OUTPUT_DIR);
+ if (!this.outputDir) {
+ return false;
+ }
+
+ let {processID, processType} = Services.appinfo;
+ this.baseFilename = `coverage-${PROCESS_TYPES[processType]}-pid_${processID}`;
+
+ if (!isContent) {
+ OS.File.profileBeforeChange.addBlocker("WebExtension coverage data flush",
+ () => this.saveAllCoverage());
+
+ Services.ppmm.addMessageListener(COVERAGE_MESSAGE, this, true);
+ } else {
+ Services.obs.addObserver(this, "content-child-shutdown", false);
+ }
+
+ return true;
+ },
+
+ observe() {
+ this.saveAllCoverage();
+ },
+
+ receiveMessage({data}) {
+ for (let [key, coverage] of data.entries()) {
+ this.writeCoverageFile(key, coverage);
+ }
+ },
+
+ register(global) {
+ if (!(COVERAGE_PROPERTY in global)) {
+ return;
+ }
+ if (!this.init()) {
+ return;
+ }
+
+ let name = [this.baseFilename,
+ mangleFilename(Components.stack.caller.filename),
+ globalID++].join("-");
+
+ let outputFile = `${name}.json`;
+
+ this.coverageObjects.set(outputFile, global[COVERAGE_PROPERTY]);
+
+ if ("addEventListener" in global) {
+ let listener = () => {
+ global.removeEventListener("unload", listener);
+ this.saveCoverage(outputFile, true);
+ };
+ global.addEventListener("unload", listener);
+ }
+ },
+
+ writeCoverageFile(filename, coverage) {
+ if (isContent) {
+ Services.cpmm.sendAsyncMessage(
+ COVERAGE_MESSAGE,
+ new Map([[filename, coverage]]));
+
+ return Promise.resolve();
+ }
+
+ let path = OS.Path.join(this.outputDir, filename);
+ let promise = OS.File.writeAtomic(path, JSON.stringify(coverage),
+ {tmpPath: `${path}.tmp`});
+
+ this.promises.add(promise);
+ return promise.then(() => {
+ this.promises.delete(promise);
+ });
+ },
+
+ saveCoverage(filename, finalize = true) {
+ let coverage = this.coverageObjects.get(filename);
+ if (finalize) {
+ this.coverageObjects.delete(filename);
+ }
+
+ return this.writeCoverageFile(filename, coverage);
+ },
+
+ saveAllCoverage(finalize = true) {
+ if (isContent) {
+ Services.cpmm.sendAsyncMessage(COVERAGE_MESSAGE, this.coverageObjects);
+ return Promise.resolve();
+ }
+
+ for (let filename of this.coverageObjects.keys()) {
+ this.saveCoverage(filename, finalize);
+ }
+
+ return Promise.all(this.promises);
+ },
+};
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -7,16 +7,19 @@
const EXPORTED_SYMBOLS = ["WebNavigation"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
// Maximum amount of time that can be passed and still consider
// the data recent (similar to how is done in nsNavHistory,
// e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
const RECENT_DATA_THRESHOLD = 5 * 1000000;
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -1,15 +1,18 @@
"use strict";
/* globals docShell */
var Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
function loadListener(event) {
let document = event.target;
let window = document.defaultView;
let url = document.documentURI;
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -3,16 +3,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EXPORTED_SYMBOLS = ["WebNavigationFrames"];
var Ci = Components.interfaces;
+Components.utils.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
/* exported WebNavigationFrames */
function getWindowId(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -13,16 +13,19 @@ const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
const {nsIHttpActivityObserver, nsISocketTransport} = Ci;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
"resource://gre/modules/WebRequestCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload",
--- a/toolkit/modules/addons/WebRequestCommon.jsm
+++ b/toolkit/modules/addons/WebRequestCommon.jsm
@@ -8,16 +8,20 @@ const EXPORTED_SYMBOLS = ["WebRequestCom
/* exported WebRequestCommon */
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
var WebRequestCommon = {
typeForPolicyType(type) {
switch (type) {
case Ci.nsIContentPolicy.TYPE_DOCUMENT: return "main_frame";
case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT: return "sub_frame";
case Ci.nsIContentPolicy.TYPE_STYLESHEET: return "stylesheet";
case Ci.nsIContentPolicy.TYPE_SCRIPT: return "script";
case Ci.nsIContentPolicy.TYPE_IMAGE: return "image";
--- a/toolkit/modules/addons/WebRequestContent.js
+++ b/toolkit/modules/addons/WebRequestContent.js
@@ -6,16 +6,19 @@
var Ci = Components.interfaces;
var Cc = Components.classes;
var Cu = Components.utils;
var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
"resource://gre/modules/WebRequestCommon.jsm");
const IS_HTTP = /^https?:/;
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -14,16 +14,17 @@ TESTING_JS_MODULES += [
'tests/PromiseTestUtils.jsm',
'tests/xpcshell/TestIntegration.jsm',
]
SPHINX_TREES['toolkit_modules'] = 'docs'
EXTRA_JS_MODULES += [
'addons/MatchPattern.jsm',
+ 'addons/WebExtCoverage.jsm',
'addons/WebNavigation.jsm',
'addons/WebNavigationContent.js',
'addons/WebNavigationFrames.jsm',
'addons/WebRequest.jsm',
'addons/WebRequestCommon.jsm',
'addons/WebRequestContent.js',
'addons/WebRequestUpload.jsm',
'AsyncPrefs.jsm',