From 91d6531c40f74eee333330cd803c86c3286c4a81 Mon Sep 17 00:00:00 2001 From: Beth Rennie Date: Tue, 11 Mar 2025 18:16:50 +0000 Subject: [PATCH] Bug 1942694 - Remove toolkit/components/featuregates r=settings-reviewers,firefox-desktop-core-reviewers ,frontend-codestyle-reviewers,Gijs,android-reviewers,Roger Differential Revision: https://phabricator.services.mozilla.com/D239672 --- .eslintrc-rollouts.js | 2 - browser/components/BrowserGlue.sys.mjs | 3 - docs/conf.py | 1 - .../components/lib/crash/metrics.yaml | 18 - .../service/GleanCrashReporterService.kt | 13 - .../service/GleanCrashReporterServiceTest.kt | 5 - pyproject.toml | 1 - taskcluster/kinds/source-test/python.yml | 22 - .../crashes/CrashManager.in.sys.mjs | 1 - toolkit/components/crashes/metrics.yaml | 18 - .../tests/xpcshell/test_crash_manager.js | 5 - .../featuregates/FeatureGate.sys.mjs | 279 ------------ .../FeatureGateImplementation.sys.mjs | 245 ----------- toolkit/components/featuregates/Features.toml | 49 --- .../components/featuregates/docs/index.rst | 179 -------- .../featuregates/gen_feature_definitions.py | 165 ------- toolkit/components/featuregates/jar.mn | 9 - toolkit/components/featuregates/moz.build | 20 - .../test/python/data/empty_feature.toml | 1 - .../featuregates/test/python/data/good.toml | 18 - .../test/python/data/invalid_toml.toml | 1 - .../featuregates/test/python/python.toml | 4 - .../python/test_gen_feature_definitions.py | 281 ------------ .../components/featuregates/test/unit/head.js | 3 - .../test/unit/test_FeatureGate.js | 412 ------------------ .../unit/test_FeatureGateImplementation.js | 141 ------ .../featuregates/test/unit/xpcshell.toml | 12 - toolkit/components/moz.build | 2 +- .../telemetry/docs/data/crash-ping.rst | 2 +- toolkit/content/aboutSupport.js | 27 -- toolkit/content/aboutSupport.xhtml | 21 - toolkit/crashreporter/CrashAnnotations.yaml | 6 - .../client/app/src/net/ping/glean.rs | 3 +- toolkit/docs/index.rst | 1 - .../en-US/toolkit/about/aboutSupport.ftl | 3 - toolkit/modules/Troubleshoot.sys.mjs | 21 - .../tests/browser/browser_Troubleshoot.js | 32 -- toolkit/toolkit.mozbuild | 1 - tools/lint/test-manifest-toml.yml | 1 - 39 files changed, 3 insertions(+), 2025 deletions(-) delete mode 100644 toolkit/components/featuregates/FeatureGate.sys.mjs delete mode 100644 toolkit/components/featuregates/FeatureGateImplementation.sys.mjs delete mode 100644 toolkit/components/featuregates/Features.toml delete mode 100644 toolkit/components/featuregates/docs/index.rst delete mode 100755 toolkit/components/featuregates/gen_feature_definitions.py delete mode 100644 toolkit/components/featuregates/jar.mn delete mode 100644 toolkit/components/featuregates/moz.build delete mode 100644 toolkit/components/featuregates/test/python/data/empty_feature.toml delete mode 100644 toolkit/components/featuregates/test/python/data/good.toml delete mode 100644 toolkit/components/featuregates/test/python/data/invalid_toml.toml delete mode 100644 toolkit/components/featuregates/test/python/python.toml delete mode 100644 toolkit/components/featuregates/test/python/test_gen_feature_definitions.py delete mode 100644 toolkit/components/featuregates/test/unit/head.js delete mode 100644 toolkit/components/featuregates/test/unit/test_FeatureGate.js delete mode 100644 toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js delete mode 100644 toolkit/components/featuregates/test/unit/xpcshell.toml diff --git a/.eslintrc-rollouts.js b/.eslintrc-rollouts.js index 194dffcd7e9e..2f5e9e20a87a 100644 --- a/.eslintrc-rollouts.js +++ b/.eslintrc-rollouts.js @@ -362,7 +362,6 @@ const rollouts = [ "toolkit/components/crashmonitor/CrashMonitor.sys.mjs", "toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs", "toolkit/components/downloads/**", - "toolkit/components/featuregates/FeatureGate*.*", "toolkit/components/forgetaboutsite/test/unit/test_removeDataFromDomain.js", "toolkit/components/glean/tests/browser/**", "toolkit/components/kvstore/kvstore.sys.mjs", @@ -513,7 +512,6 @@ const rollouts = [ "toolkit/components/downloads/**", "toolkit/components/enterprisepolicies/EnterprisePolicies*.sys.mjs", "toolkit/components/extensions/**", - "toolkit/components/featuregates/**", "toolkit/components/forgetaboutsite/**", "toolkit/components/formautofill/**", "toolkit/components/glean/tests/browser/**", diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index 5ffb07f8fded..d77cf453aab5 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -37,7 +37,6 @@ ChromeUtils.defineESModuleGetters(lazy, { DownloadsViewableInternally: "resource:///modules/DownloadsViewableInternally.sys.mjs", ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs", - FeatureGate: "resource://featuregates/FeatureGate.sys.mjs", FirefoxBridgeExtensionUtils: "resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs", // FilePickerCrashed is used by the `listeners` object below. @@ -2387,8 +2386,6 @@ BrowserGlue.prototype = { if (AppConstants.MOZ_CRASHREPORTER) { lazy.UnsubmittedCrashHandler.init(); lazy.UnsubmittedCrashHandler.scheduleCheckForUnsubmittedCrashReports(); - lazy.FeatureGate.annotateCrashReporter(); - lazy.FeatureGate.observePrefChangesForCrashReportAnnotation(); } if (AppConstants.ASAN_REPORTER) { diff --git a/docs/conf.py b/docs/conf.py index 38c6bf803075..ddf878fbbf19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -74,7 +74,6 @@ js_source_path = [ "../toolkit/actors", "../toolkit/components/extensions", "../toolkit/components/extensions/parent", - "../toolkit/components/featuregates", "../toolkit/components/ml/content/backends/ONNXPipeline.mjs", "../toolkit/mozapps/extensions", "../toolkit/components/prompts/src", diff --git a/mobile/android/android-components/components/lib/crash/metrics.yaml b/mobile/android/android-components/components/lib/crash/metrics.yaml index 2d8d7f1074fb..ca11f35d9103 100644 --- a/mobile/android/android-components/components/lib/crash/metrics.yaml +++ b/mobile/android/android-components/components/lib/crash/metrics.yaml @@ -822,24 +822,6 @@ memory: environment: - experimental_features: - type: string_list - description: > - Specifies the enabled experimental features from - about:preferences#experimental. - notification_emails: - - crash-reporting-wg@mozilla.org - - stability@mozilla.org - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1830098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1830098 - data_sensitivity: - - technical - expires: never - send_in_pings: - - crash - headless_mode: type: boolean description: > diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt index 4721dc401181..46326f505b02 100644 --- a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt @@ -40,7 +40,6 @@ import mozilla.components.support.ktx.android.content.isMainProcess import mozilla.telemetry.glean.private.BooleanMetricType import mozilla.telemetry.glean.private.ObjectMetricType import mozilla.telemetry.glean.private.QuantityMetricType -import mozilla.telemetry.glean.private.StringListMetricType import mozilla.telemetry.glean.private.StringMetricType import java.io.File import java.io.FileOutputStream @@ -149,9 +148,6 @@ class GleanCrashReporterService( // Overrides the original `startup` parameter to `Ping` when present GleanCrash.startup.setIfNonNull(extras["StartupCrash"]) - GleanEnvironment.experimentalFeatures.setExperimentalFeaturesIfNonNull( - extras["ExperimentalFeatures"], - ) GleanEnvironment.headlessMode.setIfNonNull(extras["HeadlessMode"]) GleanMemory.availableCommit.setIfNonNull(extras["AvailablePageFile"]) @@ -180,15 +176,6 @@ class GleanCrashReporterService( element?.jsonPrimitive?.long?.let(::set) } - private fun StringListMetricType.setExperimentalFeaturesIfNonNull( - element: JsonElement?, - ) { - element?.let { - // Split on commas - set(it.jsonPrimitive.content.split(',').filter(String::isNotEmpty)) - } - } - private fun ObjectMetricType.setAsyncShutdownTimeoutIfNonNull( element: JsonElement?, ) { diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt index 4c6b79186938..f6f68d13dd48 100644 --- a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt +++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt @@ -639,7 +639,6 @@ class GleanCrashReporterServiceTest { "Version": "123.0.0", "StartupCrash": "1", "TotalPhysicalMemory": 100, - "ExperimentalFeatures": "expa,expb", "AsyncShutdownTimeout": "{\"phase\":\"abcd\",\"conditions\":[{\"foo\":\"bar\"}],\"brokenAddBlockers\":[\"foo\"]}", "QuotaManagerShutdownTimeout": "line1\nline2\nline3", "StackTraces": $stackTracesAnnotation @@ -677,10 +676,6 @@ class GleanCrashReporterServiceTest { assertEquals("beta", GleanCrash.appChannel.testGetValue()) assertEquals("123.0.0", GleanCrash.appDisplayVersion.testGetValue()) assertEquals(100L, GleanMemory.totalPhysical.testGetValue()) - assertEquals( - listOf("expa", "expb"), - GleanEnvironment.experimentalFeatures.testGetValue(), - ) assertEquals( JsonObject( mapOf( diff --git a/pyproject.toml b/pyproject.toml index 3f4ce79fb0c9..5b682f570091 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ exclude = [ "testing/runtimes/writeruntimes.py", "testing/tools/iceserver/iceserver.py", "testing/tools/websocketprocessbridge/websocketprocessbridge.py", - "toolkit/components/featuregates", "toolkit/content/tests/chrome/file_about_networking_wsh.py", "toolkit/library/build/dependentlibs.py", "toolkit/mozapps", diff --git a/taskcluster/kinds/source-test/python.yml b/taskcluster/kinds/source-test/python.yml index be1c05818bb2..665534303acf 100644 --- a/taskcluster/kinds/source-test/python.yml +++ b/taskcluster/kinds/source-test/python.yml @@ -534,28 +534,6 @@ condprof: - 'testing/condprofile/condprof**' - 'testing/condprofile/setup.py' -featuregates: - description: featuregates Python unit tests - platform: - - linux1804-64/opt - - windows11-64/opt - python-version: [3] - treeherder: - symbol: fg - run: - using: python-test - subsuite: featuregates - fetches: - toolchain: - by-platform: - linux1804-64/opt: - - linux64-node - windows11-64/opt: - - win64-node - when: - files-changed: - - 'toolkit/components/featuregates/**' - skip-fails: description: testing/skip-fails unit tests always-target: false diff --git a/toolkit/components/crashes/CrashManager.in.sys.mjs b/toolkit/components/crashes/CrashManager.in.sys.mjs index f713623ce31b..b47fede18052 100644 --- a/toolkit/components/crashes/CrashManager.in.sys.mjs +++ b/toolkit/components/crashes/CrashManager.in.sys.mjs @@ -869,7 +869,6 @@ CrashManager.prototype = Object.freeze({ user32LoadedBefore: t(bool, "User32BeforeBlocklist"), }, environment: { - experimentalFeatures: t(comma_list, cap), headlessMode: t(bool, cap), nimbusEnrollments: t(comma_list, cap), uptime: t(seconds, "UptimeTS"), diff --git a/toolkit/components/crashes/metrics.yaml b/toolkit/components/crashes/metrics.yaml index e71b049eb171..4874982cb3fa 100644 --- a/toolkit/components/crashes/metrics.yaml +++ b/toolkit/components/crashes/metrics.yaml @@ -832,24 +832,6 @@ environment: send_in_pings: - crash - experimental_features: - type: string_list - description: > - Specifies the enabled experimental features from - about:preferences#experimental. - notification_emails: - - crash-reporting-wg@mozilla.org - - stability@mozilla.org - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1830098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1830098 - data_sensitivity: - - technical - expires: never - send_in_pings: - - crash - headless_mode: type: boolean description: > diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js index 69ce6977d6bf..948a18520f1f 100644 --- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js +++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js @@ -975,10 +975,6 @@ add_task(async function test_glean_crash_ping() { ]); Assert.equal(Glean.dllBlocklist.initFailed.testGetValue(), true); Assert.equal(Glean.dllBlocklist.user32LoadedBefore.testGetValue(), true); - Assert.deepEqual(Glean.environment.experimentalFeatures.testGetValue(), [ - "feature 1", - "feature 2", - ]); Assert.equal(Glean.environment.headlessMode.testGetValue(), true); Assert.deepEqual(Glean.environment.nimbusEnrollments.testGetValue(), [ "foo:control", @@ -1022,7 +1018,6 @@ add_task(async function test_glean_crash_ping() { BlockedDllList: "Foo.dll;bar.dll;rawr.dll", BlocklistInitFailed: "1", EventLoopNestingLevel: 5, - ExperimentalFeatures: "feature 1,feature 2", FontName: "Helvetica", GPUProcessLaunchCount: 10, HeadlessMode: "1", diff --git a/toolkit/components/featuregates/FeatureGate.sys.mjs b/toolkit/components/featuregates/FeatureGate.sys.mjs deleted file mode 100644 index 590c80c2db9b..000000000000 --- a/toolkit/components/featuregates/FeatureGate.sys.mjs +++ /dev/null @@ -1,279 +0,0 @@ -/* 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/. */ - -import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - // This is an unfortunate exception where we depend on ASRouter because - // Nimbus has this dependency. - // This implementation is written in a way where it will avoid requiring - // this module if it's not available. - ASRouterTargeting: - // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit - "resource:///modules/asrouter/ASRouterTargeting.sys.mjs", - FeatureGateImplementation: - "resource://featuregates/FeatureGateImplementation.sys.mjs", - ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs", - TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs", -}); - -ChromeUtils.defineLazyGetter(lazy, "gFeatureDefinitionsPromise", async () => { - const url = "resource://featuregates/feature_definitions.json"; - return fetchFeatureDefinitions(url); -}); - -const kCustomTargeting = { - // For default values, although something like `channel == 'nightly'` kinda - // works, local builds don't have that update channel set in that way so it - // doesn't, and then tests fail because the defaults for the FeatureGate - // do not match the default value in the prefs code. - // We may in future want other things from AppConstants here, too. - nightly_build: AppConstants.NIGHTLY_BUILD, - thunderbird: AppConstants.MOZ_APP_NAME == "thunderbird", -}; - -ChromeUtils.defineLazyGetter(lazy, "defaultContexts", () => { - let ASRouterEnv = {}; - try { - ASRouterEnv = lazy.ASRouterTargeting.Environment; - } catch (ex) { - // No ASRouter; just keep going. - } - return [ - kCustomTargeting, - lazy.ExperimentManager.createTargetingContext(), - ASRouterEnv, - ]; -}); - -function getCombinedContext(...contexts) { - let combined = lazy.TargetingContext.combineContexts( - ...lazy.defaultContexts, - ...contexts - ); - return new lazy.TargetingContext(combined, { - source: "featuregate", - }); -} - -async function fetchFeatureDefinitions(url) { - const res = await fetch(url); - let definitionsJson = await res.json(); - return new Map(Object.entries(definitionsJson)); -} - -async function buildFeatureGateImplementation(definition) { - const targetValueKeys = ["defaultValue", "isPublic"]; - for (const key of targetValueKeys) { - definition[key] = await FeatureGate.evaluateJexlValue( - definition[key + "Jexl"] - ); - } - return new lazy.FeatureGateImplementation(definition); -} - -let featureGatePrefObserver = { - onChange() { - FeatureGate.annotateCrashReporter(); - }, - // Ignore onEnable and onDisable since onChange is called in both cases. - onEnable() {}, - onDisable() {}, -}; - -const kFeatureGateCache = new Map(); - -/** A high level control for turning features on and off. */ -export class FeatureGate { - /* - * This is structured as a class with static methods to that sphinx-js can - * easily document it. This constructor is required for sphinx-js to detect - * this class for documentation. - */ - - constructor() {} - - /** - * Constructs a feature gate object that is defined in ``Features.toml``. - * This is the primary way to create a ``FeatureGate``. - * - * @param {string} id The ID of the feature's definition in `Features.toml`. - * @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests. - * @throws If the ``id`` passed is not defined in ``Features.toml``. - */ - static async fromId(id, testDefinitionsUrl = undefined) { - let featureDefinitions; - if (testDefinitionsUrl) { - featureDefinitions = await fetchFeatureDefinitions(testDefinitionsUrl); - } else { - featureDefinitions = await lazy.gFeatureDefinitionsPromise; - } - - if (!featureDefinitions.has(id)) { - throw new Error( - `Unknown feature id ${id}. Features must be defined in toolkit/components/featuregates/Features.toml` - ); - } - - // Make a copy of the definition, since we are about to modify it - return buildFeatureGateImplementation({ ...featureDefinitions.get(id) }); - } - - /** - * Constructs feature gate objects for each of the definitions in ``Features.toml``. - * @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests. - */ - static async all(testDefinitionsUrl = undefined) { - let featureDefinitions; - if (testDefinitionsUrl) { - featureDefinitions = await fetchFeatureDefinitions(testDefinitionsUrl); - } else { - featureDefinitions = await lazy.gFeatureDefinitionsPromise; - } - - let definitions = []; - for (let definition of featureDefinitions.values()) { - // Make a copy of the definition, since we are about to modify it - definitions[definitions.length] = await buildFeatureGateImplementation( - Object.assign({}, definition) - ); - } - return definitions; - } - - static async observePrefChangesForCrashReportAnnotation( - testDefinitionsUrl = undefined - ) { - let featureDefinitions = await FeatureGate.all(testDefinitionsUrl); - - for (let definition of featureDefinitions.values()) { - FeatureGate.addObserver( - definition.id, - featureGatePrefObserver, - testDefinitionsUrl - ); - } - } - - static async annotateCrashReporter() { - if (!Services.appinfo.crashReporterEnabled) { - return; - } - let features = await FeatureGate.all(); - let enabledFeatures = []; - for (let feature of features) { - if (await feature.getValue()) { - enabledFeatures.push(feature.preference); - } - } - Services.appinfo.annotateCrashReport( - "ExperimentalFeatures", - enabledFeatures.join(",") - ); - } - - /** - * Add an observer for a feature gate by ID. If the feature is of type - * boolean and currently enabled, `onEnable` will be called. - * - * The underlying feature gate instance will be shared with all other callers - * of this function, and share an observer. - * - * @param {string} id The ID of the feature's definition in `Features.toml`. - * @param {object} observer Functions to be called when the feature changes. - * All observer functions are optional. - * @param {Function()} [observer.onEnable] Called when the feature becomes enabled. - * @param {Function()} [observer.onDisable] Called when the feature becomes disabled. - * @param {Function(newValue)} [observer.onChange] Called when the - * feature's state changes to any value. The new value will be passed to the - * function. - * @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests. - * @returns {Promise} The current value of the feature. - */ - static async addObserver(id, observer, testDefinitionsUrl = undefined) { - if (!kFeatureGateCache.has(id)) { - kFeatureGateCache.set( - id, - await FeatureGate.fromId(id, testDefinitionsUrl) - ); - } - const feature = kFeatureGateCache.get(id); - return feature.addObserver(observer); - } - - /** - * Remove an observer of changes from this feature - * @param {string} id The ID of the feature's definition in `Features.toml`. - * @param observer Then observer that was passed to addObserver to remove. - */ - static async removeObserver(id, observer) { - let feature = kFeatureGateCache.get(id); - if (!feature) { - return; - } - feature.removeObserver(observer); - if (feature._observers.size === 0) { - kFeatureGateCache.delete(id); - } - } - - /** - * Get the current value of this feature gate. Implementors should avoid - * storing the result to avoid missing changes to the feature's value. - * Consider using :func:`addObserver` if it is necessary to store the value - * of the feature. - * - * @async - * @param {string} id The ID of the feature's definition in `Features.toml`. - * @returns {Promise} A promise for the value associated with this feature. - */ - static async getValue(id, testDefinitionsUrl = undefined) { - let feature = kFeatureGateCache.get(id); - if (!feature) { - feature = await FeatureGate.fromId(id, testDefinitionsUrl); - } - return feature.getValue(); - } - - /** - * An alias of `getValue` for boolean typed feature gates. - * - * @async - * @param {string} id The ID of the feature's definition in `Features.toml`. - * @returns {Promise} A promise for the value associated with this feature. - * @throws {Error} If the feature is not a boolean. - */ - static async isEnabled(id, testDefinitionsUrl = undefined) { - let feature = kFeatureGateCache.get(id); - if (!feature) { - feature = await FeatureGate.fromId(id, testDefinitionsUrl); - } - return feature.isEnabled(); - } - - /** - * Take a jexl expression and evaluate it against the standard Nimbus - * context, extended with some additional properties defined in - * kCustomTargeting. - * - * @param {String} jexlExpression The expression to evaluate. - * @param {Object[]?} additionalContexts Any additional context properties - * that should be taken into account. - * - * @returns {Promise} Resolves to either true or false if successful, - * or null if there was some problem with the jexl expression (which - * will also log an error to the console). - */ - static async evaluateJexlValue(jexlExpression, ...additionalContexts) { - let result = null; - let context = getCombinedContext(...additionalContexts); - try { - result = !!(await context.evalWithDefault(jexlExpression)); - } catch (ex) { - console.error(ex); - } - return result; - } -} diff --git a/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs b/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs deleted file mode 100644 index 247b03323c2a..000000000000 --- a/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs +++ /dev/null @@ -1,245 +0,0 @@ -/* 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/. */ - -/** An individual feature gate that can be re-used for more advanced usage. */ -export class FeatureGateImplementation { - // Note that the following comment is *not* a jsdoc. Making it a jsdoc would - // makes sphinx-js expose it to users. This feature shouldn't be used by - // users, and so should not be in the docs. Sphinx-js does not respect the - // @private marker on a constructor (https://github.com/erikrose/sphinx-js/issues/71). - /* - * This constructor should only be used directly in tests. - * ``FeatureGate.fromId`` should be used instead for most cases. - * - * @private - * - * @param {object} definition Description of the feature gate. - * @param {string} definition.id - * @param {string} definition.title - * @param {string} definition.description - * @param {string} definition.descriptionLinks - * @param {boolean} definition.restartRequired - * @param {string} definition.type - * @param {string} definition.preference - * @param {string} definition.defaultValue - * @param {object} definition.isPublic - * @param {object} definition.bugNumbers - */ - constructor(definition) { - this._definition = definition; - this._observers = new Set(); - } - - // The below are all getters instead of direct access to make it easy to provide JSDocs. - - /** - * A short string used to refer to this feature in code. - * @type string - */ - get id() { - return this._definition.id; - } - - /** - * A Fluent string ID that will resolve to some text to identify this feature's group to users. - * @type string - */ - get group() { - return this._definition.group; - } - - /** - * A Fluent string ID that will resolve to some text to identify this feature to users. - * @type string - */ - get title() { - return this._definition.title; - } - - /** - * A Fluent string ID that will resolve to a longer string to show to users that explains the feature. - * @type string - */ - get description() { - return this._definition.description; - } - - get descriptionLinks() { - return this._definition.descriptionLinks; - } - - /** - * Whether this feature requires a browser restart to take effect after toggling. - * @type boolean - */ - get restartRequired() { - return this._definition.restartRequired; - } - - /** - * The type of feature. Currently only booleans are supported. This may be - * richer than JS types in the future, such as enum values. - * @type string - */ - get type() { - return this._definition.type; - } - - /** - * The name of the preference that stores the value of this feature. - * - * This preference should not be read directly, but instead its values should - * be accessed via FeatureGate#addObserver or FeatureGate#getValue. This - * property is provided for backwards compatibility. - * - * @type string - */ - get preference() { - return this._definition.preference; - } - - /** - * The default value for the feature gate for this update channel. - * @type boolean - */ - get defaultValue() { - return this._definition.defaultValue; - } - - /** - * If this feature should be exposed to users in an advanced settings panel - * for this build of Firefox. - * - * @type boolean - */ - get isPublic() { - return this._definition.isPublic; - } - - /** - * Bug numbers associated with this feature. - * @type Array - */ - get bugNumbers() { - return this._definition.bugNumbers; - } - - /** - * Get the current value of this feature gate. Implementors should avoid - * storing the result to avoid missing changes to the feature's value. - * Consider using :func:`addObserver` if it is necessary to store the value - * of the feature. - * - * @async - * @returns {Promise} A promise for the value associated with this feature. - */ - // Note that this is async for potential future use of a storage backend besides preferences. - async getValue() { - return Services.prefs.getBoolPref(this.preference, this.defaultValue); - } - - /** - * An alias of `getValue` for boolean typed feature gates. - * - * @async - * @returns {Promise} A promise for the value associated with this feature. - * @throws {Error} If the feature is not a boolean. - */ - // Note that this is async for potential future use of a storage backend besides preferences. - async isEnabled() { - if (this.type !== "boolean") { - throw new Error( - `Tried to call isEnabled when type is not boolean (it is ${this.type})` - ); - } - return this.getValue(); - } - - /** - * Add an observer for changes to this feature. When the observer is added, - * `onChange` will asynchronously be called with the current value of the - * preference. If the feature is of type boolean and currently enabled, - * `onEnable` will additionally be called. - * - * @param {object} observer Functions to be called when the feature changes. - * All observer functions are optional. - * @param {Function()} [observer.onEnable] Called when the feature becomes enabled. - * @param {Function()} [observer.onDisable] Called when the feature becomes disabled. - * @param {Function(newValue: boolean)} [observer.onChange] Called when the - * feature's state changes to any value. The new value will be passed to the - * function. - * @returns {Promise} The current value of the feature. - */ - async addObserver(observer) { - if (this._observers.size === 0) { - Services.prefs.addObserver(this.preference, this); - } - - this._observers.add(observer); - - if (this.type === "boolean" && (await this.isEnabled())) { - this._callObserverMethod(observer, "onEnable"); - } - // onDisable should not be called, because features should be assumed - // disabled until onEnabled is called for the first time. - - return this.getValue(); - } - - /** - * Remove an observer of changes from this feature - * @param observer The observer that was passed to addObserver to remove. - */ - removeObserver(observer) { - this._observers.delete(observer); - if (this._observers.size === 0) { - Services.prefs.removeObserver(this.preference, this); - } - } - - /** - * Removes all observers from this instance of the feature gate. - */ - removeAllObservers() { - if (this._observers.size > 0) { - this._observers.clear(); - Services.prefs.removeObserver(this.preference, this); - } - } - - _callObserverMethod(observer, method, ...args) { - if (method in observer) { - try { - observer[method](...args); - } catch (err) { - console.error(err); - } - } - } - - /** - * Observes changes to the preference storing the enabled state of the - * feature. The observer is dynamically added only when observer have been - * added. - * @private - */ - async observe(aSubject, aTopic, aData) { - if (aTopic === "nsPref:changed" && aData === this.preference) { - const value = await this.getValue(); - for (const observer of this._observers) { - this._callObserverMethod(observer, "onChange", value); - - if (value) { - this._callObserverMethod(observer, "onEnable"); - } else { - this._callObserverMethod(observer, "onDisable"); - } - } - } else { - console.error( - new Error(`Unexpected event observed: ${aSubject}, ${aTopic}, ${aData}`) - ); - } - } -} diff --git a/toolkit/components/featuregates/Features.toml b/toolkit/components/featuregates/Features.toml deleted file mode 100644 index a15e98995bf6..000000000000 --- a/toolkit/components/featuregates/Features.toml +++ /dev/null @@ -1,49 +0,0 @@ -# is-public-jexl and default-value-jexl are both JEXL expressions. -# They are evaluated using the Experimenter environment, plus some -# additional variables defined by ASRouter and FeatureGate. -# The result of evaluating is-public-jexl is exposed as `isPublic` -# and controls whether the feature is shown to the user in e.g. -# the relevant pane in Firefox's settings. -# The result of evaluating default-value-jexl is exposed as -# `defaultValue` and should match the default value of the pref -# as set in the relevant .js prefs file or StaticPrefList.yaml. -# -# NOTE ON ORDERING -# The ordering of the groups and controls in about:preferences is -# determined by the ordering in this file. Groups will be ordered -# based on the order of appearance in this file. Settings within -# a group will also be determined by their order in this file. - -[auto-pip] -group = "experimental-features-group-customize-browsing" -title = "experimental-features-auto-pip" -description = "experimental-features-auto-pip-description" -restart-required = false -preference = "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled" -type = "boolean" -bug-numbers = [1647800] -is-public-jexl = "true" -default-value-jexl = "false" - -[url-bar-ime-search] -group = "experimental-features-group-customize-browsing" -title = "experimental-features-ime-search" -description = "experimental-features-ime-search-description" -restart-required = false -preference = "browser.urlbar.keepPanelOpenDuringImeComposition" -type = "boolean" -bug-numbers = [1673971] -is-public-jexl = "true" -default-value-jexl = "false" - -[media-jxl] -group = "experimental-features-group-webpage-display" -title = "experimental-features-media-jxl" -description = "experimental-features-media-jxl-description" -description-links = {bugzilla = "https://bugzilla.mozilla.org/show_bug.cgi?id=1539075"} -restart-required = false -preference = "image.jxl.enabled" -type = "boolean" -bug-numbers = [1539075] -is-public-jexl = "nightly_build" -default-value-jexl = "false" diff --git a/toolkit/components/featuregates/docs/index.rst b/toolkit/components/featuregates/docs/index.rst deleted file mode 100644 index ef285f60b6e2..000000000000 --- a/toolkit/components/featuregates/docs/index.rst +++ /dev/null @@ -1,179 +0,0 @@ -.. _components/featuregates: - -============= -Feature Gates -============= - -A feature gate is a high level tool to turn features on and off. It provides -metadata about features, a simple, opinionated API, and avoid many potential -pitfalls of other systems, such as using preferences directly. It is designed -to be compatible with tools that want to know and affect the state of -features in Firefox over time and in the wild. - -Feature Definitions -=================== - -All features must have a definition, specified in -``toolkit/components/featuregates/Features.toml``. These definitions include -data such as references to title and description strings (to be shown to users), -and bug numbers (to track the development of the feature over time). Here is an -example feature definition with an id of ``demo-feature``: - -.. code-block:: toml - - [demo-feature] - title = "experimental-features-demo-feature" - description = "experimental-features-demo-feature-description" - restart-required = false - bug-numbers = [1479127] - type = "boolean" - is-public = {default = false, nightly = true} - default-value = {default = false, nightly = true} - -The references defined in the `title` and `description` values point to strings -stored in ``toolkit/locales/en-US/toolkit/featuregates/features.ftl``. The `title` -string should specify the user-facing string as the `label` attribute. - -.. _targeted value: - -Targeted values ---------------- - -Several fields can take a value that indicates it varies by channel and OS. -These are known as *targeted values*. The simplest computed value is to -simply provide the value: - -.. code-block:: toml - - default-value = true - -A more interesting example is to make a feature default to true on Nightly, -but be disabled otherwise. That would look like this: - -.. code-block:: toml - - default-value = {default = false, nightly = true} - -Values can depend on multiple conditions. For example, to enable a feature -only on Nightly running on Windows: - -.. code-block:: toml - - default-value = {default = false, "nightly,win" = true} - -Multiple sets of conditions can be specified, however use caution here: if -multiple sets could match (except ``default``), the set chosen is undefined. -An example of safely using multiple conditions: - -.. code-block:: toml - - default-value = {default = false, nightly = true, "beta,win" = true} - -The ``default`` condition is required. It is used as a fallback in case no -more-specific case matches. The conditions allowed are - -* ``default`` -* ``release`` -* ``beta`` -* ``dev-edition`` -* ``nightly`` -* ``esr`` -* ``win`` -* ``mac`` -* ``linux`` -* ``android`` - -Fields ------- - -title - Required. The string ID of the human readable name for the feature, meant to be shown to - users. Should fit onto a single line. The actual string should be defined in - ``toolkit/locales/en-US/toolkit/featuregates/features.ftl`` with the user-facing value - defined as the `label` attribute of the string. - -description - Required. The string ID of the human readable description for the feature, meant to be shown to - users. Should be at most a paragraph. The actual string should be defined in - ``toolkit/locales/en-US/toolkit/featuregates/features.ftl``. - -description-links - Optional. A dictionary of key-value pairs that are referenced in the description. The key - name must appear in the description localization text as - . For example in Features.toml: - -.. code-block:: toml - - [demo-feature] - title = "experimental-features-demo-feature" - description = "experimental-features-demo-feature-description" - description-links = {exampleCom = "https://example.com", exampleOrg = "https://example.org"} - restart-required = false - bug-numbers = [1479127] - type = "boolean" - is-public = {default = false, nightly = true} - default-value = {default = false, nightly = true} - -and in features.ftl: - -.. code-block:: fluent - - experimental-features-demo-feature = - .label = Example Demo Feature - experimental-features-demo-feature-description = Example demo feature that can point to .com links and .org links. - -bug-numbers - Required. A list of bug numbers related to this feature. This should - likely be the metabug for the the feature, but any related bugs can be - included. At least one bug is required. - -restart-required - Required. If this feature requires a the browser to be restarted for changes - to take effect, this field should be ``true``. Otherwise, the field should - be ``false``. Features should aspire to not require restarts and react to - changes to the preference dynamically. - -type - Required. The type of value this feature relates to. The only legal value is - ``boolean``, but more may be added in the future. - -preference - Optional. The preference used to track the feature. If a preference is not - provided, one will be automatically generated based on the feature ID. It is - not recommended to specify a preference directly, except to integrate with - older code. In the future, alternate storage mechanisms may be used if a - preference is not supplied. - -default-value - Optional. This is a `targeted value`_ describing - the value for the feature if no other changes have been made, such as in - a fresh profile. If not provided, the default for a boolean type feature - gate will be ``false`` for all profiles. - -is-public - Optional. This is a `targeted value`_ describing - on which branches this feature should be exposed to users. When a feature - is made public, it may show up in a future UI that allows users to opt-in - to experimental features. This is not related to ``about:preferences`` or - ``about:config``. If not provided, the default is to make a feature - private for all channels. - - -Feature Gate API -================ - -.. - (comment) The below lists should be kept in sync with the contents of the - classes they are documenting. An explicit list is used so that the - methods can be put in a particular order. - -.. js:autoclass:: FeatureGate - :members: addObserver, removeObserver, isEnabled, fromId - -.. js:autoclass:: FeatureGateImplementation - :members: id, title, description, type, bugNumbers, isPublic, defaultValue, restartRequired, preference, addObserver, removeObserver, removeAllObservers, getValue, isEnabled - - Feature implementors should use the methods :func:`fromId`, - :func:`addListener`, :func:`removeListener` and - :func:`removeAllListeners`. Additionally, metadata is available for UI and - analysis. diff --git a/toolkit/components/featuregates/gen_feature_definitions.py b/toolkit/components/featuregates/gen_feature_definitions.py deleted file mode 100755 index 491b302afb55..000000000000 --- a/toolkit/components/featuregates/gen_feature_definitions.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python -# 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/. - -import json -import re -import sys -import toml -import voluptuous -import voluptuous.humanize -from voluptuous import Schema, Optional, Any, All, Required, Length, Range, Msg, Match - - -Text = Any(str, bytes) - - -id_regex = re.compile(r"^[a-z0-9-]+$") -feature_schema = Schema( - { - Match(id_regex): { - Required("group"): All(Text, Length(min=1)), - Required("title"): All(Text, Length(min=1)), - Required("description"): All(Text, Length(min=1)), - Required("bug-numbers"): All(Length(min=1), [All(int, Range(min=1))]), - Required("restart-required"): bool, - Required("type"): "boolean", # In the future this may include other types - Optional("preference"): Text, - Optional("default-value-jexl"): Text, - Optional("is-public-jexl"): Text, - Optional("description-links"): dict, - }, - } -) - - -EXIT_OK = 0 -EXIT_ERROR = 1 - - -def main(output, *filenames): - features = {} - errors = False - try: - features = process_files(filenames) - json.dump(features, output) - except ExceptionGroup as error_group: - print(str(error_group)) - return EXIT_ERROR - return EXIT_OK - - -class ExceptionGroup(Exception): - def __init__(self, errors): - self.errors = errors - - def __str__(self): - rv = ["There were errors while processing feature definitions:"] - for error in self.errors: - # indent the message - s = "\n".join(" " + line for line in str(error).split("\n")) - # add a * at the beginning of the first line - s = " * " + s[4:] - rv.append(s) - return "\n".join(rv) - - -class FeatureGateException(Exception): - def __init__(self, message, filename=None): - super(FeatureGateException, self).__init__(message) - self.filename = filename - - def __str__(self): - message = super(FeatureGateException, self).__str__() - rv = ["In"] - if self.filename is None: - rv.append("unknown file:") - else: - rv.append('file "{}":\n'.format(self.filename)) - rv.append(message) - return " ".join(rv) - - def __repr__(self): - # Turn "FeatureGateExcept(,)" into "FeatureGateException(, filename=)" - original = super(FeatureGateException, self).__repr__() - with_comma = original[:-1] - # python 2 adds a trailing comma and python 3 does not, so we need to conditionally reinclude it - if len(with_comma) > 0 and with_comma[-1] != ",": - with_comma = with_comma + "," - return with_comma + " filename={!r})".format(self.filename) - - -def process_files(filenames): - features = {} - errors = [] - - for filename in filenames: - try: - with open(filename, "r") as f: - feature_data = toml.load(f) - - voluptuous.humanize.validate_with_humanized_errors( - feature_data, feature_schema - ) - - for feature_id, feature in feature_data.items(): - feature["id"] = feature_id - features[feature_id] = expand_feature(feature) - except ( - voluptuous.error.Error, - IOError, - FeatureGateException, - toml.TomlDecodeError, - ) as e: - # Wrap errors in enough information to know which file they came from - errors.append(FeatureGateException(e, filename)) - - if errors: - raise ExceptionGroup(errors) - - return features - - -def hyphens_to_camel_case(s): - """Convert names-with-hyphens to namesInCamelCase""" - rv = "" - for part in s.split("-"): - if rv == "": - rv = part.lower() - else: - rv += part[0].upper() + part[1:].lower() - return rv - - -def expand_feature(feature): - """Fill in default values for optional fields""" - - # convert all names-with-hyphens to namesInCamelCase - key_changes = [] - for key in feature.keys(): - if "-" in key: - new_key = hyphens_to_camel_case(key) - key_changes.append((key, new_key)) - - for old_key, new_key in key_changes: - feature[new_key] = feature[old_key] - del feature[old_key] - - if feature["type"] == "boolean": - feature.setdefault("preference", "features.{}.enabled".format(feature["id"])) - # set default value to None so that we can test for preferences where we forgot to set the default value - feature.setdefault("defaultValueJexl", None) - elif "preference" not in feature: - raise FeatureGateException( - "Features of type {} must specify an explicit preference name".format( - feature["type"] - ) - ) - - feature.setdefault("isPublicJexl", "false") - return feature - - -if __name__ == "__main__": - sys.exit(main(sys.stdout, *sys.argv[1:])) diff --git a/toolkit/components/featuregates/jar.mn b/toolkit/components/featuregates/jar.mn deleted file mode 100644 index 419d7d99f44e..000000000000 --- a/toolkit/components/featuregates/jar.mn +++ /dev/null @@ -1,9 +0,0 @@ -# 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/. - -toolkit.jar: -% resource featuregates %featuregates/ - featuregates/FeatureGate.sys.mjs (./FeatureGate.sys.mjs) - featuregates/FeatureGateImplementation.sys.mjs (./FeatureGateImplementation.sys.mjs) - featuregates/feature_definitions.json (./feature_definitions.json) diff --git a/toolkit/components/featuregates/moz.build b/toolkit/components/featuregates/moz.build deleted file mode 100644 index 475d6c72de09..000000000000 --- a/toolkit/components/featuregates/moz.build +++ /dev/null @@ -1,20 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# 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/. - -with Files("**"): - BUG_COMPONENT = ("Toolkit", "FeatureGate") - -SPHINX_TREES["featuregates"] = "docs" - -XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"] - -JAR_MANIFESTS += ["jar.mn"] - -GeneratedFile( - "feature_definitions.json", - script="gen_feature_definitions.py", - inputs=["Features.toml"], -) diff --git a/toolkit/components/featuregates/test/python/data/empty_feature.toml b/toolkit/components/featuregates/test/python/data/empty_feature.toml deleted file mode 100644 index cd89859d1fdf..000000000000 --- a/toolkit/components/featuregates/test/python/data/empty_feature.toml +++ /dev/null @@ -1 +0,0 @@ -[empty-feature] diff --git a/toolkit/components/featuregates/test/python/data/good.toml b/toolkit/components/featuregates/test/python/data/good.toml deleted file mode 100644 index 32e355b88fd9..000000000000 --- a/toolkit/components/featuregates/test/python/data/good.toml +++ /dev/null @@ -1,18 +0,0 @@ -[demo-feature] -title = "demo-feature-title" -group = "demo-feature-group" -description = "demo-feature-description" -restart-required = false -preference = "foo.bar.baz" -type = "boolean" -bug-numbers = [1479127] -is-public-jexl = "true" -default-value-jexl = "false" - -[minimal-feature] -title = "minimal-feature-title" -group = "minimal-feature-group" -description = "minimal-feature-description" -restart-required = true -type = "boolean" -bug-numbers = [1479127] diff --git a/toolkit/components/featuregates/test/python/data/invalid_toml.toml b/toolkit/components/featuregates/test/python/data/invalid_toml.toml deleted file mode 100644 index d4a8001e58bb..000000000000 --- a/toolkit/components/featuregates/test/python/data/invalid_toml.toml +++ /dev/null @@ -1 +0,0 @@ -this: is: not: valid: toml diff --git a/toolkit/components/featuregates/test/python/python.toml b/toolkit/components/featuregates/test/python/python.toml deleted file mode 100644 index 34a3aff0e9a4..000000000000 --- a/toolkit/components/featuregates/test/python/python.toml +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -subsuite = "featuregates" - -["test_gen_feature_definitions.py"] diff --git a/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py b/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py deleted file mode 100644 index f7adef2c2b4a..000000000000 --- a/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py +++ /dev/null @@ -1,281 +0,0 @@ -import json -import sys -import unittest -from os import path -from textwrap import dedent - -import mozunit -import toml -import voluptuous -from io import StringIO - - -FEATURE_GATES_ROOT_PATH = path.abspath( - path.join(path.dirname(__file__), path.pardir, path.pardir) -) -sys.path.append(FEATURE_GATES_ROOT_PATH) -from gen_feature_definitions import ( - ExceptionGroup, - expand_feature, - feature_schema, - FeatureGateException, - hyphens_to_camel_case, - main, - process_files, -) - - -def make_test_file_path(name): - return path.join(FEATURE_GATES_ROOT_PATH, "test", "python", "data", name + ".toml") - - -def minimal_definition(**kwargs): - defaults = { - "id": "test-feature", - "title": "Test Feature", - "group": "Test Feature Group", - "description": "A feature for testing things", - "bug-numbers": [1479127], - "restart-required": False, - "type": "boolean", - } - defaults.update(dict([(k.replace("_", "-"), v) for k, v in kwargs.items()])) - return defaults - - -class TestHyphensToCamelCase(unittest.TestCase): - simple_cases = [ - ("", ""), - ("singleword", "singleword"), - ("more-than-one-word", "moreThanOneWord"), - ] - - def test_simple_cases(self): - for in_string, out_string in self.simple_cases: - assert hyphens_to_camel_case(in_string) == out_string - - -class TestExceptionGroup(unittest.TestCase): - def test_str_indentation_of_grouped_lines(self): - errors = [ - Exception("single line error 1"), - Exception("single line error 2"), - Exception("multiline\nerror 1"), - Exception("multiline\nerror 2"), - ] - - assert str(ExceptionGroup(errors)) == dedent( - """\ - There were errors while processing feature definitions: - * single line error 1 - * single line error 2 - * multiline - error 1 - * multiline - error 2""" - ) - - -class TestFeatureGateException(unittest.TestCase): - def test_str_no_file(self): - error = FeatureGateException("oops") - assert str(error) == "In unknown file: oops" - - def test_str_with_file(self): - error = FeatureGateException("oops", filename="some/bad/file.txt") - assert str(error) == 'In file "some/bad/file.txt":\n oops' - - def test_repr_no_file(self): - error = FeatureGateException("oops") - assert repr(error) == "FeatureGateException('oops', filename=None)" - - def test_repr_with_file(self): - error = FeatureGateException("oops", filename="some/bad/file.txt") - assert ( - repr(error) == "FeatureGateException('oops', filename='some/bad/file.txt')" - ) - - -class TestProcessFiles(unittest.TestCase): - def test_valid_file(self): - filename = make_test_file_path("good") - result = process_files([filename]) - assert result == { - "demo-feature": { - "id": "demo-feature", - "title": "demo-feature-title", - "group": "demo-feature-group", - "description": "demo-feature-description", - "restartRequired": False, - "preference": "foo.bar.baz", - "type": "boolean", - "bugNumbers": [1479127], - "isPublicJexl": "true", - "defaultValueJexl": "false", - }, - "minimal-feature": { - "id": "minimal-feature", - "title": "minimal-feature-title", - "group": "minimal-feature-group", - "description": "minimal-feature-description", - "restartRequired": True, - "preference": "features.minimal-feature.enabled", - "type": "boolean", - "bugNumbers": [1479127], - "isPublicJexl": "false", - "defaultValueJexl": None, - }, - } - - def test_invalid_toml(self): - filename = make_test_file_path("invalid_toml") - with self.assertRaises(ExceptionGroup) as context: - process_files([filename]) - error_group = context.exception - assert len(error_group.errors) == 1 - assert type(error_group.errors[0]) == FeatureGateException - - def test_empty_feature(self): - filename = make_test_file_path("empty_feature") - with self.assertRaises(ExceptionGroup) as context: - process_files([filename]) - error_group = context.exception - assert len(error_group.errors) == 1 - assert type(error_group.errors[0]) == FeatureGateException - assert "required key not provided" in str(error_group.errors[0]) - - def test_missing_file(self): - filename = make_test_file_path("file_does_not_exist") - with self.assertRaises(ExceptionGroup) as context: - process_files([filename]) - error_group = context.exception - assert len(error_group.errors) == 1 - assert type(error_group.errors[0]) == FeatureGateException - assert "No such file or directory" in str(error_group.errors[0]) - - -class TestFeatureSchema(unittest.TestCase): - def make_test_features(self, *overrides): - if len(overrides) == 0: - overrides = [{}] - features = {} - for override in overrides: - feature = minimal_definition(**override) - feature_id = feature.pop("id") - features[feature_id] = feature - return features - - def test_minimal_valid(self): - definition = self.make_test_features() - # should not raise an exception - feature_schema(definition) - - def test_extra_keys_not_allowed(self): - definition = self.make_test_features({"unexpected_key": "oh no!"}) - with self.assertRaises(voluptuous.Error) as context: - feature_schema(definition) - assert "extra keys not allowed" in str(context.exception) - - def test_required_fields(self): - required_keys = [ - "title", - "description", - "bug-numbers", - "restart-required", - "type", - ] - for key in required_keys: - definition = self.make_test_features({"id": "test-feature"}) - del definition["test-feature"][key] - with self.assertRaises(voluptuous.Error) as context: - feature_schema(definition) - assert "required key not provided" in str(context.exception) - assert key in str(context.exception) - - def test_nonempty_keys(self): - test_parameters = [("title", ""), ("description", ""), ("bug-numbers", [])] - for key, empty in test_parameters: - definition = self.make_test_features({key: empty}) - with self.assertRaises(voluptuous.Error) as context: - feature_schema(definition) - assert "length of value must be at least" in str(context.exception) - assert "['{}']".format(key) in str(context.exception) - - -class ExpandFeatureTests(unittest.TestCase): - def test_hyphenation_to_snake_case(self): - feature = minimal_definition() - assert "bug-numbers" in feature - assert "bugNumbers" in expand_feature(feature) - - def test_default_value_default(self): - feature = minimal_definition(type="boolean") - assert "default-value" not in feature - assert "defaultValue" not in feature - assert "default-value-jexl" not in feature - assert "defaultValueJexl" not in feature - assert expand_feature(feature)["defaultValueJexl"] == None - - def test_default_value_override_constant(self): - feature = minimal_definition(type="boolean", default_value_jexl="true") - assert expand_feature(feature)["defaultValueJexl"] == "true" - - def test_default_value_override_configured_value(self): - feature = minimal_definition( - type="boolean", default_value_jexl="channel == nightly" - ) - assert expand_feature(feature)["defaultValueJexl"] == "channel == nightly" - - def test_preference_default(self): - feature = minimal_definition(type="boolean") - assert "preference" not in feature - assert expand_feature(feature)["preference"] == "features.test-feature.enabled" - - def test_preference_override(self): - feature = minimal_definition(preference="test.feature.a") - assert expand_feature(feature)["preference"] == "test.feature.a" - - -class MainTests(unittest.TestCase): - def test_it_outputs_json(self): - output = StringIO() - filename = make_test_file_path("good") - main(output, filename) - output.seek(0) - results = json.load(output) - assert results == { - "demo-feature": { - "id": "demo-feature", - "title": "demo-feature-title", - "group": "demo-feature-group", - "description": "demo-feature-description", - "restartRequired": False, - "preference": "foo.bar.baz", - "type": "boolean", - "bugNumbers": [1479127], - "isPublicJexl": "true", - "defaultValueJexl": "false", - }, - "minimal-feature": { - "id": "minimal-feature", - "title": "minimal-feature-title", - "group": "minimal-feature-group", - "description": "minimal-feature-description", - "restartRequired": True, - "preference": "features.minimal-feature.enabled", - "type": "boolean", - "bugNumbers": [1479127], - "isPublicJexl": "false", - "defaultValueJexl": None, - }, - } - - def test_it_returns_1_for_errors(self): - output = StringIO() - filename = make_test_file_path("invalid_toml") - assert main(output, filename) == 1 - assert output.getvalue() == "" - - -if __name__ == "__main__": - mozunit.main(*sys.argv[1:]) diff --git a/toolkit/components/featuregates/test/unit/head.js b/toolkit/components/featuregates/test/unit/head.js deleted file mode 100644 index bd90d22f0369..000000000000 --- a/toolkit/components/featuregates/test/unit/head.js +++ /dev/null @@ -1,3 +0,0 @@ -var { sinon } = ChromeUtils.importESModule( - "resource://testing-common/Sinon.sys.mjs" -); diff --git a/toolkit/components/featuregates/test/unit/test_FeatureGate.js b/toolkit/components/featuregates/test/unit/test_FeatureGate.js deleted file mode 100644 index c61d23113501..000000000000 --- a/toolkit/components/featuregates/test/unit/test_FeatureGate.js +++ /dev/null @@ -1,412 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -const { FeatureGate } = ChromeUtils.importESModule( - "resource://featuregates/FeatureGate.sys.mjs" -); -const { HttpServer } = ChromeUtils.importESModule( - "resource://testing-common/httpd.sys.mjs" -); -const { AppConstants } = ChromeUtils.importESModule( - "resource://gre/modules/AppConstants.sys.mjs" -); - -const kDefinitionDefaults = { - id: "test-feature", - title: "Test Feature", - description: "A feature for testing", - restartRequired: false, - type: "boolean", - preference: "test.feature", - defaultValue: false, - isPublic: false, -}; - -function definitionFactory(override = {}) { - return Object.assign({}, kDefinitionDefaults, override); -} - -class DefinitionServer { - constructor(definitionOverrides = []) { - this.server = new HttpServer(); - this.server.registerPathHandler("/definitions.json", this); - this.definitions = {}; - - for (const override of definitionOverrides) { - this.addDefinition(override); - } - - this.server.start(); - registerCleanupFunction( - () => new Promise(resolve => this.server.stop(resolve)) - ); - } - - // for nsIHttpRequestHandler - handle(request, response) { - // response.setHeader("Content-Type", "application/json"); - response.write(JSON.stringify(this.definitions)); - } - - get definitionsUrl() { - const { primaryScheme, primaryHost, primaryPort } = this.server.identity; - return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`; - } - - addDefinition(overrides = {}) { - const definition = definitionFactory(overrides); - // convert targeted values, used by fromId - definition.isPublicJexl = definition.isPublic ? "!testFact" : "testFact"; - definition.defaultValueJexl = definition.defaultValue - ? "!testFact" - : "testFact"; - this.definitions[definition.id] = definition; - return definition; - } -} - -// ============================================================================ -add_task(async function testReadAll() { - const server = new DefinitionServer(); - let ids = ["test-featureA", "test-featureB", "test-featureC"]; - for (let id of ids) { - server.addDefinition({ id }); - } - let sortedIds = ids.sort(); - const features = await FeatureGate.all(server.definitionsUrl); - for (let feature of features) { - equal( - feature.id, - sortedIds.shift(), - "Features are returned in order of definition" - ); - } - equal(sortedIds.length, 0, "All features are returned when calling all()"); -}); - -// The getters and setters should read correctly from the definition -add_task(async function testReadFromDefinition() { - const server = new DefinitionServer(); - const definition = server.addDefinition({ id: "test-feature" }); - const feature = await FeatureGate.fromId( - "test-feature", - server.definitionsUrl - ); - - // simple fields - equal(feature.id, definition.id, "id should be read from definition"); - equal( - feature.title, - definition.title, - "title should be read from definition" - ); - equal( - feature.description, - definition.description, - "description should be read from definition" - ); - equal( - feature.restartRequired, - definition.restartRequired, - "restartRequired should be read from definition" - ); - equal(feature.type, definition.type, "type should be read from definition"); - equal( - feature.preference, - definition.preference, - "preference should be read from definition" - ); - - // targeted fields - equal( - feature.defaultValue, - false, - "defaultValue should be false because testFact was not provided." - ); - - equal( - feature.isPublic, - false, - "isPublic should be false because testFact was not provided." - ); - - // cleanup - Services.prefs.getDefaultBranch("").deleteBranch("test.feature"); -}); - -// Targeted values should return the correct value -add_task(async function testTargetedValues() { - const targetingFacts = { - true1: true, - true2: true, - false1: false, - false2: false, - }; - - Assert.equal( - await FeatureGate.evaluateJexlValue("true1", targetingFacts), - true, - "A true value should be reflected" - ); - Assert.equal( - await FeatureGate.evaluateJexlValue("false1", targetingFacts), - false, - "A false value should be reflected" - ); - Assert.equal( - await FeatureGate.evaluateJexlValue("false"), - false, - "Boolean literal false should work" - ); - - Assert.equal( - await FeatureGate.evaluateJexlValue("true"), - true, - "Boolean literal true should work" - ); - - Assert.equal( - await FeatureGate.evaluateJexlValue("false2 || true1", targetingFacts), - true, - "Compound expressions work." - ); - - Assert.equal( - await FeatureGate.evaluateJexlValue("testFact", {}), - false, - "Non-existing terms in the expression get coerced to bool false." - ); - Assert.equal( - await FeatureGate.evaluateJexlValue("testFact", { testFact: true }), - true, - "Providing testFact=true, the expression returns true." - ); -}); - -// getValue should work -add_task(async function testGetValue() { - equal( - Services.prefs.getPrefType("test.feature.1"), - Services.prefs.PREF_INVALID, - "Before creating the feature gate, the preference should not exist" - ); - - const server = new DefinitionServer([ - { id: "test-feature-1", defaultValue: false, preference: "test.feature.1" }, - { id: "test-feature-2", defaultValue: true, preference: "test.feature.2" }, - ]); - - equal( - await FeatureGate.getValue("test-feature-1", server.definitionsUrl), - false, - "getValue() starts by returning the default value" - ); - equal( - await FeatureGate.getValue("test-feature-2", server.definitionsUrl), - true, - "getValue() starts by returning the default value" - ); - - Services.prefs.setBoolPref("test.feature.1", true); - equal( - await FeatureGate.getValue("test-feature-1", server.definitionsUrl), - true, - "getValue() return the new value" - ); - - Services.prefs.setBoolPref("test.feature.1", false); - equal( - await FeatureGate.getValue("test-feature-1", server.definitionsUrl), - false, - "getValue() should return the second value" - ); - - // cleanup - Services.prefs.getDefaultBranch("").deleteBranch("test.feature."); -}); - -// getValue should work -add_task(async function testGetValue() { - const server = new DefinitionServer([ - { id: "test-feature-1", defaultValue: false, preference: "test.feature.1" }, - { id: "test-feature-2", defaultValue: true, preference: "test.feature.2" }, - ]); - - equal( - Services.prefs.getPrefType("test.feature.1"), - Services.prefs.PREF_INVALID, - "Before creating the feature gate, the first preference should not exist" - ); - equal( - Services.prefs.getPrefType("test.feature.2"), - Services.prefs.PREF_INVALID, - "Before creating the feature gate, the second preference should not exist" - ); - - equal( - await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl), - false, - "isEnabled() starts by returning the default value" - ); - equal( - await FeatureGate.isEnabled("test-feature-2", server.definitionsUrl), - true, - "isEnabled() starts by returning the default value" - ); - - Services.prefs.setBoolPref("test.feature.1", true); - equal( - await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl), - true, - "isEnabled() return the new value" - ); - - Services.prefs.setBoolPref("test.feature.1", false); - equal( - await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl), - false, - "isEnabled() should return the second value" - ); - - // cleanup - Services.prefs.getDefaultBranch("").deleteBranch("test.feature."); -}); - -// adding and removing event observers should work -add_task(async function testGetValue() { - const preference = "test.pref"; - const server = new DefinitionServer([ - { id: "test-feature", defaultValue: false, preference }, - ]); - const observer = { - onChange: sinon.stub(), - onEnable: sinon.stub(), - onDisable: sinon.stub(), - }; - - let rv = await FeatureGate.addObserver( - "test-feature", - observer, - server.definitionsUrl - ); - equal(rv, false, "addObserver returns the current value"); - - Assert.deepEqual(observer.onChange.args, [], "onChange should not be called"); - Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called"); - Assert.deepEqual( - observer.onDisable.args, - [], - "onDisable should not be called" - ); - - Services.prefs.setBoolPref(preference, true); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true]], - "onChange should be called with the new value" - ); - Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called"); - Assert.deepEqual( - observer.onDisable.args, - [], - "onDisable should not be called" - ); - - Services.prefs.setBoolPref(preference, false); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true], [false]], - "onChange should be called again with the new value" - ); - Assert.deepEqual( - observer.onEnable.args, - [[]], - "onEnable should not be called a second time" - ); - Assert.deepEqual( - observer.onDisable.args, - [[]], - "onDisable should be called for the first time" - ); - - Services.prefs.setBoolPref(preference, false); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true], [false]], - "onChange should not be called if the value did not change" - ); - Assert.deepEqual( - observer.onEnable.args, - [[]], - "onEnable should not be called again if the value did not change" - ); - Assert.deepEqual( - observer.onDisable.args, - [[]], - "onDisable should not be called if the value did not change" - ); - - // remove the listener and make sure the observer isn't called again - FeatureGate.removeObserver("test-feature", observer); - await Promise.resolve(); // Allow events to be called async - - Services.prefs.setBoolPref(preference, true); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true], [false]], - "onChange should not be called after observer was removed" - ); - Assert.deepEqual( - observer.onEnable.args, - [[]], - "onEnable should not be called after observer was removed" - ); - Assert.deepEqual( - observer.onDisable.args, - [[]], - "onDisable should not be called after observer was removed" - ); - - // cleanup - Services.prefs.getDefaultBranch("").deleteBranch(preference); -}); - -if (AppConstants.platform != "android") { - // All preferences should have default values. - add_task(async function testAllHaveDefault() { - const featuresList = await FeatureGate.all(); - for (let feature of featuresList) { - notEqual( - typeof feature.defaultValue, - "undefined", - `Feature ${feature.id} should have a defined default value!` - ); - notEqual( - feature.defaultValue, - null, - `Feature ${feature.id} should have a non-null default value!` - ); - } - }); - - // All preference defaults should match service pref defaults - add_task(async function testAllDefaultsMatchSettings() { - const featuresList = await FeatureGate.all(); - for (let feature of featuresList) { - let value = Services.prefs - .getDefaultBranch("") - .getBoolPref(feature.preference); - equal( - feature.defaultValue, - value, - `Feature ${feature.preference} should match runtime value.` - ); - } - }); -} diff --git a/toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js b/toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js deleted file mode 100644 index ac844b488029..000000000000 --- a/toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js +++ /dev/null @@ -1,141 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -const { FeatureGateImplementation } = ChromeUtils.importESModule( - "resource://featuregates/FeatureGateImplementation.sys.mjs" -); - -const kDefinitionDefaults = { - id: "test-feature", - title: "Test Feature", - description: "A feature for testing", - restartRequired: false, - type: "boolean", - preference: "test.feature", - defaultValue: false, - isPublic: false, -}; - -function definitionFactory(override = {}) { - return Object.assign({}, kDefinitionDefaults, override); -} - -// getValue should work -add_task(async function testGetValue() { - const preference = "test.pref"; - equal( - Services.prefs.getPrefType(preference), - Services.prefs.PREF_INVALID, - "Before creating the feature gate, the preference should not exist" - ); - const feature = new FeatureGateImplementation( - definitionFactory({ preference, defaultValue: false }) - ); - equal( - Services.prefs.getPrefType(preference), - Services.prefs.PREF_INVALID, - "Instantiating a feature gate should not set its default value" - ); - equal( - await feature.getValue(), - false, - "getValue() should return the feature gate's default" - ); - - Services.prefs.setBoolPref(preference, true); - equal( - await feature.getValue(), - true, - "getValue() should return the new value" - ); - - Services.prefs.setBoolPref(preference, false); - equal( - await feature.getValue(), - false, - "getValue() should return the third value" - ); - - // cleanup - Services.prefs.getDefaultBranch("").deleteBranch(preference); -}); - -// event observers should work -add_task(async function testGetValue() { - const preference = "test.pref"; - const feature = new FeatureGateImplementation( - definitionFactory({ preference, defaultValue: false }) - ); - const observer = { - onChange: sinon.stub(), - onEnable: sinon.stub(), - onDisable: sinon.stub(), - }; - - let rv = await feature.addObserver(observer); - equal(rv, false, "addObserver returns the current value"); - - Assert.deepEqual(observer.onChange.args, [], "onChange should not be called"); - Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called"); - Assert.deepEqual( - observer.onDisable.args, - [], - "onDisable should not be called" - ); - - Services.prefs.setBoolPref(preference, true); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true]], - "onChange should be called with the new value" - ); - Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called"); - Assert.deepEqual( - observer.onDisable.args, - [], - "onDisable should not be called" - ); - - Services.prefs.setBoolPref(preference, false); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true], [false]], - "onChange should be called again with the new value" - ); - Assert.deepEqual( - observer.onEnable.args, - [[]], - "onEnable should not be called a second time" - ); - Assert.deepEqual( - observer.onDisable.args, - [[]], - "onDisable should be called for the first time" - ); - - Services.prefs.setBoolPref(preference, false); - await Promise.resolve(); // Allow events to be called async - Assert.deepEqual( - observer.onChange.args, - [[true], [false]], - "onChange should not be called if the value did not change" - ); - Assert.deepEqual( - observer.onEnable.args, - [[]], - "onEnable should not be called again if the value did not change" - ); - Assert.deepEqual( - observer.onDisable.args, - [[]], - "onDisable should not be called if the value did not change" - ); - - // cleanup - feature.removeAllObservers(); - Services.prefs.getDefaultBranch("").deleteBranch(preference); -}); diff --git a/toolkit/components/featuregates/test/unit/xpcshell.toml b/toolkit/components/featuregates/test/unit/xpcshell.toml deleted file mode 100644 index 6fc53b0a2fe0..000000000000 --- a/toolkit/components/featuregates/test/unit/xpcshell.toml +++ /dev/null @@ -1,12 +0,0 @@ -[DEFAULT] -head = "head.js" -tags = "featuregates" -firefox-appdir = "browser" - -["test_FeatureGate.js"] -# Ignore platforms which the use the update channel 'default' on non-nightly -# platforms because it gets compared to preference values guarded by variables -# like RELEASE_OR_BETA which are set based on the build channel. -skip-if = ["!nightly_build && (asan || debug)"] - -["test_FeatureGateImplementation.js"] diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index 24291461548e..98c335b9dbad 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -134,7 +134,7 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] not in ("android", "windows"): DIRS += ["aboutwebauthn"] if CONFIG["MOZ_BUILD_APP"] == "browser": - DIRS += ["featuregates", "messaging-system", "normandy"] + DIRS += ["messaging-system", "normandy"] DIRS += ["nimbus"] diff --git a/toolkit/components/telemetry/docs/data/crash-ping.rst b/toolkit/components/telemetry/docs/data/crash-ping.rst index bae800f54f61..a8a124d2772a 100644 --- a/toolkit/components/telemetry/docs/data/crash-ping.rst +++ b/toolkit/components/telemetry/docs/data/crash-ping.rst @@ -68,7 +68,6 @@ Structure: CrashTime: