author | Julian Descottes <jdescottes@mozilla.com> |
Fri, 02 Oct 2020 15:39:44 +0000 | |
changeset 551361 | 87389dede385c168717b46d17357699f5180c2e5 |
parent 551360 | 06e10dd4a6c42274800f957048457577c3788ce1 |
child 551362 | 26b597ec35bb6a9e0da25cfb58a70d2504e48d18 |
push id | 127820 |
push user | jdescottes@mozilla.com |
push date | Fri, 02 Oct 2020 22:08:32 +0000 |
treeherder | autoland@ba4685a82eea [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nchevobbe |
bugs | 1668117 |
milestone | 83.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/debugger/jest-test.config.js +++ b/devtools/client/debugger/jest-test.config.js @@ -35,16 +35,17 @@ module.exports = { setupFiles: ["<rootDir>/src/test/shim.js", "jest-localstorage-mock"], snapshotSerializers: [ "jest-serializer-babel-ast", "enzyme-to-json/serializer", ], moduleNameMapper: { "\\.css$": "<rootDir>/src/test/__mocks__/styleMock.js", "\\.svg$": "<rootDir>/src/test/__mocks__/svgMock.js", + "devtools-services": "<rootDir>/src/test/fixtures/Services", "^Services": "<rootDir>/src/test/fixtures/Services", "^chrome": "<rootDir>/src/test/fixtures/Chrome", "^ChromeUtils": "<rootDir>/src/test/fixtures/ChromeUtils", "\\/plural-form$": "<rootDir>/src/test/fixtures/plural-form", "shared\\/telemetry$": "<rootDir>/src/test/fixtures/telemetry", "\\/unicode-url$": "<rootDir>/src/test/fixtures/unicode-url", // Map all require("devtools/...") to the real devtools root. "^devtools\\/(.*)": "<rootDir>/../../$1",
--- a/devtools/client/debugger/src/test/fixtures/Services.js +++ b/devtools/client/debugger/src/test/fixtures/Services.js @@ -1,11 +1,533 @@ /* 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"; -module.exports = { +/* globals localStorage, window, document, NodeFilter */ + +// XXX: This file is a copy of the Services shim from devtools-services. +// See https://github.com/firefox-devtools/devtools-core/blob/a9263b4c3f88ea42879a36cdc3ca8217b4a528ea/packages/devtools-services/index.js +// Many Jest tests in the debugger rely on preferences, but can't use Services. +// This fixture is probably doing too much and should be reduced to the minimum +// needed to pass the tests. + +// Some constants from nsIPrefBranch.idl. +const PREF_INVALID = 0; +const PREF_STRING = 32; +const PREF_INT = 64; +const PREF_BOOL = 128; +const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; + +// We prefix all our local storage items with this. +const PREFIX = "Services.prefs:"; + +/** + * Create a new preference branch. This object conforms largely to + * nsIPrefBranch and nsIPrefService, though it only implements the + * subset needed by devtools. A preference branch can hold child + * preferences while also holding a preference value itself. + * + * @param {PrefBranch} parent the parent branch, or null for the root + * branch. + * @param {String} name the base name of this branch + * @param {String} fullName the fully-qualified name of this branch + */ +function PrefBranch(parent, name, fullName) { + this._parent = parent; + this._name = name; + this._fullName = fullName; + this._observers = {}; + this._children = {}; + + // Properties used when this branch has a value as well. + this._defaultValue = null; + this._hasUserValue = false; + this._userValue = null; + this._type = PREF_INVALID; +} + +PrefBranch.prototype = { + PREF_INVALID: PREF_INVALID, + PREF_STRING: PREF_STRING, + PREF_INT: PREF_INT, + PREF_BOOL: PREF_BOOL, + + /** @see nsIPrefBranch.root. */ + get root() { + return this._fullName; + }, + + /** @see nsIPrefBranch.getPrefType. */ + getPrefType: function(prefName) { + return this._findPref(prefName)._type; + }, + + /** @see nsIPrefBranch.getBoolPref. */ + getBoolPref: function(prefName, defaultValue) { + try { + let thePref = this._findPref(prefName); + if (thePref._type !== PREF_BOOL) { + throw new Error(`${prefName} does not have bool type`); + } + return thePref._get(); + } catch (e) { + if (typeof defaultValue !== "undefined") { + return defaultValue; + } + throw e; + } + }, + + /** @see nsIPrefBranch.setBoolPref. */ + setBoolPref: function(prefName, value) { + if (typeof value !== "boolean") { + throw new Error("non-bool passed to setBoolPref"); + } + let thePref = this._findOrCreatePref(prefName, value, true, value); + if (thePref._type !== PREF_BOOL) { + throw new Error(`${prefName} does not have bool type`); + } + thePref._set(value); + }, + + /** @see nsIPrefBranch.getCharPref. */ + getCharPref: function(prefName, defaultValue) { + try { + let thePref = this._findPref(prefName); + if (thePref._type !== PREF_STRING) { + throw new Error(`${prefName} does not have string type`); + } + return thePref._get(); + } catch (e) { + if (typeof defaultValue !== "undefined") { + return defaultValue; + } + throw e; + } + }, + + /** @see nsIPrefBranch.getStringPref. */ + getStringPref: function() { + return this.getCharPref.apply(this, arguments); + }, + + /** @see nsIPrefBranch.setCharPref. */ + setCharPref: function(prefName, value) { + if (typeof value !== "string") { + throw new Error("non-string passed to setCharPref"); + } + let thePref = this._findOrCreatePref(prefName, value, true, value); + if (thePref._type !== PREF_STRING) { + throw new Error(`${prefName} does not have string type`); + } + thePref._set(value); + }, + + /** @see nsIPrefBranch.setStringPref. */ + setStringPref: function() { + return this.setCharPref.apply(this, arguments); + }, + + /** @see nsIPrefBranch.getIntPref. */ + getIntPref: function(prefName, defaultValue) { + try { + let thePref = this._findPref(prefName); + if (thePref._type !== PREF_INT) { + throw new Error(`${prefName} does not have int type`); + } + return thePref._get(); + } catch (e) { + if (typeof defaultValue !== "undefined") { + return defaultValue; + } + throw e; + } + }, + + /** @see nsIPrefBranch.setIntPref. */ + setIntPref: function(prefName, value) { + if (typeof value !== "number") { + throw new Error("non-number passed to setIntPref"); + } + let thePref = this._findOrCreatePref(prefName, value, true, value); + if (thePref._type !== PREF_INT) { + throw new Error(`${prefName} does not have int type`); + } + thePref._set(value); + }, + + /** @see nsIPrefBranch.clearUserPref */ + clearUserPref: function(prefName) { + let thePref = this._findPref(prefName); + thePref._clearUserValue(); + }, + + /** @see nsIPrefBranch.prefHasUserValue */ + prefHasUserValue: function(prefName) { + let thePref = this._findPref(prefName); + return thePref._hasUserValue; + }, + + /** @see nsIPrefBranch.addObserver */ + addObserver: function(domain, observer, holdWeak) { + if (holdWeak) { + throw new Error("shim prefs only supports strong observers"); + } + + if (!(domain in this._observers)) { + this._observers[domain] = []; + } + this._observers[domain].push(observer); + }, + + /** @see nsIPrefBranch.removeObserver */ + removeObserver: function(domain, observer) { + if (!(domain in this._observers)) { + return; + } + let index = this._observers[domain].indexOf(observer); + if (index >= 0) { + this._observers[domain].splice(index, 1); + } + }, + + /** @see nsIPrefService.savePrefFile */ + savePrefFile: function(file) { + if (file) { + throw new Error("shim prefs only supports null file in savePrefFile"); + } + // Nothing to do - this implementation always writes back. + }, + + /** @see nsIPrefService.getBranch */ + getBranch: function(prefRoot) { + if (!prefRoot) { + return this; + } + if (prefRoot.endsWith(".")) { + prefRoot = prefRoot.slice(0, -1); + } + // This is a bit weird since it could erroneously return a pref, + // not a pref branch. + return this._findPref(prefRoot); + }, + + /** + * Return this preference's current value. + * + * @return {Any} The current value of this preference. This may + * return a string, a number, or a boolean depending on the + * preference's type. + */ + _get: function() { + if (this._hasUserValue) { + return this._userValue; + } + return this._defaultValue; + }, + + /** + * Set the preference's value. The new value is assumed to be a + * user value. After setting the value, this function emits a + * change notification. + * + * @param {Any} value the new value + */ + _set: function(value) { + if (!this._hasUserValue || value !== this._userValue) { + this._userValue = value; + this._hasUserValue = true; + this._saveAndNotify(); + } + }, + + /** + * Set the default value for this preference, and emit a + * notification if this results in a visible change. + * + * @param {Any} value the new default value + */ + _setDefault: function(value) { + if (this._defaultValue !== value) { + this._defaultValue = value; + if (!this._hasUserValue) { + this._saveAndNotify(); + } + } + }, + + /** + * If this preference has a user value, clear it. If a change was + * made, emit a change notification. + */ + _clearUserValue: function() { + if (this._hasUserValue) { + this._userValue = null; + this._hasUserValue = false; + this._saveAndNotify(); + } + }, + + /** + * Helper function to write the preference's value to local storage + * and then emit a change notification. + */ + _saveAndNotify: function() { + let store = { + type: this._type, + defaultValue: this._defaultValue, + hasUserValue: this._hasUserValue, + userValue: this._userValue, + }; + + localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store)); + this._parent._notify(this._name); + }, + + /** + * Change this preference's value without writing it back to local + * storage. This is used to handle changes to local storage that + * were made externally. + * + * @param {Number} type one of the PREF_* values + * @param {Any} userValue the user value to use if the pref does not exist + * @param {Any} defaultValue the default value to use if the pref + * does not exist + * @param {Boolean} hasUserValue if a new pref is created, whether + * the default value is also a user value + * @param {Object} store the new value of the preference. It should + * be of the form {type, defaultValue, hasUserValue, userValue}; + * where |type| is one of the PREF_* type constants; |defaultValue| + * and |userValue| are the default and user values, respectively; + * and |hasUserValue| is a boolean indicating whether the user value + * is valid + */ + _storageUpdated: function(type, userValue, hasUserValue, defaultValue) { + this._type = type; + this._defaultValue = defaultValue; + this._hasUserValue = hasUserValue; + this._userValue = userValue; + // There's no need to write this back to local storage, since it + // came from there; and this avoids infinite event loops. + this._parent._notify(this._name); + }, + + /** + * Helper function to find either a Preference or PrefBranch object + * given its name. If the name is not found, throws an exception. + * + * @param {String} prefName the fully-qualified preference name + * @return {Object} Either a Preference or PrefBranch object + */ + _findPref: function(prefName) { + let branchNames = prefName.split("."); + let branch = this; + + for (let branchName of branchNames) { + branch = branch._children[branchName]; + if (!branch) { + // throw new Error(`could not find pref branch ${ prefName}`); + return false; + + } + } + + return branch; + }, + + /** + * Helper function to notify any observers when a preference has + * changed. This will also notify the parent branch for further + * reporting. + * + * @param {String} relativeName the name of the updated pref, + * relative to this branch + */ + _notify: function(relativeName) { + for (let domain in this._observers) { + if (relativeName === domain || domain === "" || + (domain.endsWith(".") && relativeName.startsWith(domain))) { + // Allow mutation while walking. + let localList = this._observers[domain].slice(); + for (let observer of localList) { + try { + if ("observe" in observer) { + observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, + relativeName); + } else { + // Function-style observer -- these aren't mentioned in + // the IDL, but they're accepted and devtools uses them. + observer(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, relativeName); + } + } catch (e) { + console.error(e); + } + } + } + } + + if (this._parent) { + this._parent._notify(`${this._name }.${ relativeName}`); + } + }, + + /** + * Helper function to create a branch given an array of branch names + * representing the path of the new branch. + * + * @param {Array} branchList an array of strings, one per component + * of the branch to be created + * @return {PrefBranch} the new branch + */ + _createBranch: function(branchList) { + let parent = this; + for (let branch of branchList) { + if (!parent._children[branch]) { + let isParentRoot = !parent._parent; + let branchName = (isParentRoot ? "" : `${parent.root }.`) + branch; + parent._children[branch] = new PrefBranch(parent, branch, branchName); + } + parent = parent._children[branch]; + } + return parent; + }, + + /** + * Create a new preference. The new preference is assumed to be in + * local storage already, and the new value is taken from there. + * + * @param {String} keyName the full-qualified name of the preference. + * This is also the name of the key in local storage. + * @param {Any} userValue the user value to use if the pref does not exist + * @param {Boolean} hasUserValue if a new pref is created, whether + * the default value is also a user value + * @param {Any} defaultValue the default value to use if the pref + * does not exist + * @param {Boolean} init if true, then this call is initialization + * from local storage and should override the default prefs + */ + _findOrCreatePref: function(keyName, userValue, hasUserValue, defaultValue, + init = false) { + let branch = this._createBranch(keyName.split(".")); + + if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) { + throw new Error(`inconsistent values when creating ${ keyName}`); + } + + let type; + switch (typeof (defaultValue)) { + case "boolean": + type = PREF_BOOL; + break; + case "number": + type = PREF_INT; + break; + case "string": + type = PREF_STRING; + break; + default: + throw new Error(`unhandled argument type: ${ typeof (defaultValue)}`); + } + + if (init || branch._type === PREF_INVALID) { + branch._storageUpdated(type, userValue, hasUserValue, defaultValue); + } else if (branch._type !== type) { + throw new Error(`attempt to change type of pref ${ keyName}`); + } + + return branch; + }, + + getKeyName: function(keyName) { + if (keyName.startsWith(PREFIX)) { + return keyName.slice(PREFIX.length); + } + + return keyName; + }, + + /** + * Helper function that is called when local storage changes. This + * updates the preferences and notifies pref observers as needed. + * + * @param {StorageEvent} event the event representing the local + * storage change + */ + _onStorageChange: function(event) { + if (event.storageArea !== localStorage) { + return; + } + + const key = this.getKeyName(event.key); + + // Ignore delete events. Not clear what's correct. + if (key === null || event.newValue === null) { + return; + } + + let { type, userValue, hasUserValue, defaultValue } = + JSON.parse(event.newValue); + if (event.oldValue === null) { + this._findOrCreatePref(key, userValue, hasUserValue, defaultValue); + } else { + let thePref = this._findPref(key); + thePref._storageUpdated(type, userValue, hasUserValue, defaultValue); + } + }, + + /** + * Helper function to initialize the root PrefBranch. + */ + _initializeRoot: function() { + if (Services._defaultPrefsEnabled) { + /* eslint-disable no-eval */ + // let devtools = require("raw!prefs!devtools/client/preferences/devtools"); + // eval(devtools); + // let all = require("raw!prefs!modules/libpref/init/all"); + // eval(all); + /* eslint-enable no-eval */ + } + + // Read the prefs from local storage and create the local + // representations. + for (let i = 0; i < localStorage.length; ++i) { + let keyName = localStorage.key(i); + if (keyName.startsWith(PREFIX)) { + let { userValue, hasUserValue, defaultValue } = + JSON.parse(localStorage.getItem(keyName)); + this._findOrCreatePref(keyName.slice(PREFIX.length), userValue, + hasUserValue, defaultValue, true); + } + } + + this._onStorageChange = this._onStorageChange.bind(this); + window.addEventListener("storage", this._onStorageChange); + }, +}; + + +const Services = { + _prefs: null, + + _defaultPrefsEnabled: true, + + get prefs() { + if (!this._prefs) { + this._prefs = new PrefBranch(null, "", ""); + this._prefs._initializeRoot(); + } + return this._prefs; + }, + appinfo: "", - prefs: { getBoolPref: () => {}, addObserver: () => {} }, - obs: { addObserver: () => {} }, + obs: { addObserver: () => {} } }; + +function pref(name, value) { + let thePref = Services.prefs._findOrCreatePref(name, value, true, value); + thePref._setDefault(value); +} + +module.exports = Services; +Services.pref = pref;
--- a/devtools/client/debugger/src/utils/prefs.js +++ b/devtools/client/debugger/src/utils/prefs.js @@ -1,15 +1,16 @@ /* 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/>. */ // @flow -import { PrefsHelper } from "devtools-modules"; +// $FlowIgnore +const { PrefsHelper } = require("devtools/client/shared/prefs"); import { isDevelopment } from "devtools-environment"; import Services from "devtools-services"; // Schema version to bump when the async store format has changed incompatibly // and old stores should be cleared. const prefsSchemaVersion = 11; const { pref } = Services;
--- a/devtools/client/shared/prefs.js +++ b/devtools/client/shared/prefs.js @@ -33,41 +33,51 @@ const EventEmitter = require("devtools/s * An object containing { accessorName: [prefType, prefName] } keys. */ function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) { EventEmitter.decorate(this); const cache = new Map(); for (const accessorName in prefsBlueprint) { - const [prefType, prefName] = prefsBlueprint[accessorName]; - map(this, cache, accessorName, prefType, prefsRoot, prefName); + const [prefType, prefName, fallbackValue] = prefsBlueprint[accessorName]; + map( + this, + cache, + accessorName, + prefType, + prefsRoot, + prefName, + fallbackValue + ); } const observer = makeObserver(this, cache, prefsRoot, prefsBlueprint); this.registerObserver = () => observer.register(); this.unregisterObserver = () => observer.unregister(); } /** * Helper method for getting a pref value. * * @param Map cache * @param string prefType * @param string prefsRoot * @param string prefName + * @param string|int|boolean fallbackValue * @return any */ -function get(cache, prefType, prefsRoot, prefName) { +function get(cache, prefType, prefsRoot, prefName, fallbackValue) { const cachedPref = cache.get(prefName); if (cachedPref !== undefined) { return cachedPref; } const value = Services.prefs["get" + prefType + "Pref"]( - [prefsRoot, prefName].join(".") + [prefsRoot, prefName].join("."), + fallbackValue ); cache.set(prefName, value); return value; } /** * Helper method for setting a pref value. * @@ -92,50 +102,53 @@ function set(cache, prefType, prefsRoot, * using the standard JSON serializer). * * @param PrefsHelper self * @param Map cache * @param string accessorName * @param string prefType * @param string prefsRoot * @param string prefName + * @param string|int|boolean fallbackValue * @param array serializer [optional] */ function map( self, cache, accessorName, prefType, prefsRoot, prefName, + fallbackValue, serializer = { in: e => e, out: e => e } ) { if (prefName in self) { throw new Error( `Can't use ${prefName} because it overrides a property` + "on the instance." ); } if (prefType == "Json") { - map(self, cache, accessorName, "Char", prefsRoot, prefName, { + map(self, cache, accessorName, "Char", prefsRoot, prefName, fallbackValue, { in: JSON.parse, out: JSON.stringify, }); return; } if (prefType == "Float") { - map(self, cache, accessorName, "Char", prefsRoot, prefName, { + map(self, cache, accessorName, "Char", prefsRoot, prefName, fallbackValue, { in: Number.parseFloat, out: n => n + "", }); return; } Object.defineProperty(self, accessorName, { - get: () => serializer.in(get(cache, prefType, prefsRoot, prefName)), + get: () => + serializer.in(get(cache, prefType, prefsRoot, prefName, fallbackValue)), set: e => set(cache, prefType, prefsRoot, prefName, serializer.out(e)), }); } /** * Finds the accessor for the provided pref, based on the blueprint object * used in the constructor. *
--- a/devtools/shared/event-emitter.js +++ b/devtools/shared/event-emitter.js @@ -344,17 +344,17 @@ module.exports = EventEmitter; const isEventHandler = listener => listener && handler in listener && typeof listener[handler] === "function"; const Services = require("Services"); const { getNthPathExcluding } = require("devtools/shared/platform/stack"); let loggingEnabled = false; if (!isWorker) { - loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit"); + loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false); const observer = { observe: () => { loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit"); }, }; Services.prefs.addObserver("devtools.dump.emit", observer); // Also listen for Loader unload to unregister the pref observer and