Backed out 13 changesets (bug 1752665, bug 1757809, bug 1757611, bug 1757778, bug 1755610, bug 1755599) for causing failures at browser_asrouter_experimentsAPILoader.js. CLOSED TREE

Backed out changeset 6cb519d823b1 (bug 1757611)
Backed out changeset ed7637200c68 (bug 1755610)
Backed out changeset 4e95d6562f3a (bug 1752665)
Backed out changeset e759d7cfc1a7 (bug 1752665)
Backed out changeset d191bf245e98 (bug 1752665)
Backed out changeset 143d1392493e (bug 1752665)
Backed out changeset 4596f3c5162b (bug 1757778)
Backed out changeset 4462dae86143 (bug 1757778)
Backed out changeset 150da87f13dc (bug 1757778)
Backed out changeset 23f2509de0e5 (bug 1757809)
Backed out changeset 638deb75892a (bug 1757809)
Backed out changeset 546699cd1765 (bug 1755599)
Backed out changeset 901b123391f9 (bug 1755599)
This commit is contained in:
Butkovits Atila 2022-03-18 23:44:34 +02:00
Родитель b18a47ce22
Коммит 524f529345
42 изменённых файлов: 318 добавлений и 2722 удалений

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

@ -28,11 +28,6 @@
"type": "string",
"description": "Background css behind modal content"
},
"logoImageURL": {
"type": "string",
"format": "uri",
"description": "(Deprecated by logo.imageURL)"
},
"logo": {
"type": "object",
"properties": {
@ -49,6 +44,14 @@
"description": "The logo size."
}
},
"oneOf": [
{
"required": ["imageURL"]
},
{
"required": ["imageId"]
}
],
"additionalProperties": false
},
"body": {
@ -207,25 +210,6 @@
}
},
"additionalProperties": false,
"if": {
"properties": {
"logoImageURL": { "type": "null" }
}
},
"then": {
"properties": {
"logo": {
"oneOf": [
{
"required": ["imageURL"]
},
{
"required": ["imageId"]
}
]
}
}
},
"required": ["template"]
},
"frequency": {

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

@ -67,7 +67,7 @@
"cta_url": {
"description": "Target URL for the What's New message.",
"type": "string",
"format": "moz-url-format"
"format": "uri"
},
"cta_type": {
"description": "Type of url open action",

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

@ -30,8 +30,7 @@
},
"infoLinkUrl": {
"type": "string",
"description": "URL for the info section link.",
"format": "moz-url-format"
"description": "URL for the info section link."
},
"promoEnabled": {
"type": "boolean",
@ -60,8 +59,7 @@
},
"promoLinkUrl": {
"type": "string",
"description": "URL for link in the promo box.",
"format": "moz-url-format"
"description": "URL for link in the promo box."
},
"promoLinkType": {
"type": "string",
@ -70,13 +68,11 @@
},
"promoImageLarge": {
"type": "string",
"description": "URL for image used on the left side of the promo box, larger, showcases some feature. Default off.",
"format": "uri"
"description": "URL for image used on the left side of the promo box, larger, showcases some feature. Default off."
},
"promoImageSmall": {
"type": "string",
"description": "URL for image used on the right side of the promo box, smaller, usually a logo. Default off.",
"format": "uri"
"description": "URL for image used on the right side of the promo box, smaller, usually a logo. Default off."
}
}
}

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

@ -11,12 +11,6 @@ browser.jar:
res/activity-stream/aboutwelcome/aboutwelcome.bundle.js (./aboutwelcome/content/aboutwelcome.bundle.js)
res/activity-stream/aboutwelcome/aboutwelcome.html (./aboutwelcome/content/aboutwelcome.html)
res/activity-stream/aboutwelcome/lib/ (./aboutwelcome/lib/*)
res/activity-stream/schemas/CFR/ExtensionDoorhanger.schema.json (./content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json)
res/activity-stream/schemas/CFR/InfoBar.schema.json (./content-src/asrouter/templates/CFR/templates/InfoBar.schema.json)
res/activity-stream/schemas/OnboardingMessage/UpdateAction.schema.json (./content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json)
res/activity-stream/schemas/OnboardingMessage/Spotlight.schema.json (./content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json)
res/activity-stream/schemas/OnboardingMessage/WhatsNewMessage.schema.json (./content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json)
res/activity-stream/schemas/PBNewtab/NewtabPromoMessage.schema.json (./content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json)
res/activity-stream/vendor/Redux.jsm (./vendor/Redux.jsm)
res/activity-stream/vendor/react.js (./vendor/react.js)
res/activity-stream/vendor/react-dom.js (./vendor/react-dom.js)

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

@ -34,7 +34,7 @@ const MESSAGES = () => [
icon_alt: { string_id: "cfr-badge-reader-label-newfeature" },
body: "Message body",
link_text: "Click here",
cta_url: "about:blank",
cta_url: "",
cta_type: "OPEN_PROTECTION_REPORT",
},
targeting: `firefoxVersion >= 72`,
@ -105,9 +105,11 @@ const MESSAGES = () => [
bucket_id: "WHATS_NEW_PIONEER_82",
published_date: 1603152000000,
title: "Put your data to work for a better internet",
icon_url: "",
icon_alt: "",
body:
"Contribute your data to Mozilla's Pioneer program to help researchers understand pressing technology issues like misinformation, data privacy, and ethical AI.",
cta_url: "about:blank",
cta_url: "pioneer",
cta_where: "tab",
cta_type: "OPEN_ABOUT_PAGE",
link_text: "Join Pioneer",

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

@ -22,7 +22,6 @@ const { TelemetryTestUtils } = ChromeUtils.import(
const MESSAGE_CONTENT = {
id: "xman_test_message",
groups: [],
content: {
text: "This is a test CFR",
addon: {
@ -42,7 +41,7 @@ const MESSAGE_CONTENT = {
},
action: {
data: {
url: "about:blank",
url: null,
},
type: "INSTALL_ADDON_FROM_URL",
},
@ -76,7 +75,6 @@ const MESSAGE_CONTENT = {
],
},
category: "cfrAddons",
layout: "short_message",
bucket_id: "CFR_M1",
info_icon: {
label: {
@ -335,3 +333,20 @@ add_task(async function test_exposure_ping_legacy() {
exposureSpy.restore();
await cleanup();
});
add_task(async function test_featureless_experiment() {
let experiment = await getCFRExperiment();
// Remove the feature property from the branch
experiment.branches.forEach(branch => delete branch.features);
Assert.ok(ExperimentAPI._store.getAllActive().length === 0, "Empty store");
await setup(experiment);
// Fetch the new recipe from RS
await RemoteSettingsExperimentLoader.updateRecipes();
// Enrollment was successful; featureless experiments shouldn't break anything
await BrowserTestUtils.waitForCondition(
() => ExperimentAPI._store.getAllActive().length === 1,
"ExperimentAPI should return an experiment"
);
await cleanup();
});

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

@ -0,0 +1,50 @@
import { PanelTestProvider } from "lib/PanelTestProvider.jsm";
import update_schema from "content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json";
import whats_new_schema from "content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json";
import spotlight_schema from "content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json";
import PBNewtabSchema from "content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json";
describe("PanelTestProvider", () => {
let messages;
beforeEach(async () => {
messages = await PanelTestProvider.getMessages();
});
it("should have correct number of messages", () => {
// Careful: when changing this number make sure that new messages also go
// through schema verifications.
assert.lengthOf(messages, 15);
});
it("should be a valid message", () => {
const updateMessages = messages.filter(
({ template }) => template === "update_action"
);
for (let message of updateMessages) {
assert.jsonSchema(message, update_schema);
}
});
it("should be a valid message", () => {
const whatsNewMessages = messages.filter(
({ template }) => template === "whatsnew_panel_message"
);
for (let message of whatsNewMessages) {
assert.jsonSchema(message.content, whats_new_schema);
// Not part of `message.content` so it can't be enforced through schema
assert.property(message, "order");
}
});
it("should be a valid spotlight message", () => {
const spotlightMessages = messages.filter(
({ template }) => template === "spotlight"
);
for (let message of spotlightMessages) {
assert.jsonSchema(message, spotlight_schema);
}
});
it("should be a valid pb newtab message", () => {
const pbNewtabMessages = messages.filter(
({ template }) => template === "pb_newtab"
);
assert.lengthOf(pbNewtabMessages, 1);
pbNewtabMessages.forEach(m => assert.jsonSchema(m, PBNewtabSchema));
});
});

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

@ -1,101 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { PanelTestProvider } = ChromeUtils.import(
"resource://activity-stream/lib/PanelTestProvider.jsm"
);
const { validate } = ChromeUtils.import(
"resource://gre/modules/JsonSchema.jsm"
);
Cu.importGlobalProperties(["fetch"]);
let UPDATE_ACTION_SCHEMA;
let WHATS_NEW_SCHEMA;
let SPOTLIGHT_SCHEMA;
let PB_NEWTAB_SCHEMA;
add_setup(async function setup() {
function fetchSchema(uri) {
return fetch(uri, { credentials: "omit" }).then(rsp => rsp.json());
}
UPDATE_ACTION_SCHEMA = await fetchSchema(
"resource://activity-stream/schemas/OnboardingMessage/UpdateAction.schema.json"
);
WHATS_NEW_SCHEMA = await fetchSchema(
"resource://activity-stream/schemas/OnboardingMessage/WhatsNewMessage.schema.json"
);
SPOTLIGHT_SCHEMA = await fetchSchema(
"resource://activity-stream/schemas/OnboardingMessage/Spotlight.schema.json"
);
PB_NEWTAB_SCHEMA = await fetchSchema(
"resource://activity-stream/schemas/PBNewtab/NewtabPromoMessage.schema.json"
);
});
function assertSchema(obj, schema, log) {
Assert.deepEqual(validate(obj, schema), { valid: true, errors: [] }, log);
}
add_task(async function test_PanelTestProvider() {
const messages = await PanelTestProvider.getMessages();
// Careful: when changing this number make sure the new messages also go
// through schema validation.
Assert.strictEqual(
messages.length,
15,
"PanelTestProvider should have the correct number of messages"
);
for (const [i, msg] of messages
.filter(m => m.template === "update_action")
.entries()) {
assertSchema(
msg,
UPDATE_ACTION_SCHEMA,
`update_action message ${msg.id ?? i} is valid`
);
}
for (const [i, msg] of messages
.filter(m => m.template === "whatsnew_panel_message")
.entries()) {
assertSchema(
msg.content,
WHATS_NEW_SCHEMA,
`whatsnew_panel_message message ${msg.id ?? i} is valid`
);
Assert.ok(
Object.keys(msg).includes("order"),
`whatsnew_panel_message message ${msg.id ?? i} has "order" property`
);
}
for (const [i, msg] of messages
.filter(m => m.template === "spotlight")
.entries()) {
assertSchema(
msg,
SPOTLIGHT_SCHEMA,
`spotlight message ${msg.id ?? i} is valid`
);
}
for (const [i, msg] of messages
.filter(m => m.template === "pb_newtab")
.entries()) {
assertSchema(
msg,
PB_NEWTAB_SCHEMA,
`pb_newtab message ${msg.id ?? i} is valid`
);
}
Assert.strictEqual(
messages.filter(m => m.template === "pb_newtab").length,
1,
"There is one pb_newtab message"
);
});

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

@ -17,4 +17,3 @@ support-files =
[test_ASRouterTargeting_attribution.js]
skip-if = toolkit != "cocoa" # osx specific tests
[test_AboutWelcomeTelemetry.js]
[test_PanelTestProvider.js]

21
third_party/js/cfworker/LICENSE.md поставляемый
Просмотреть файл

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Jeremy Danyow
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
third_party/js/cfworker/build.sh поставляемый
Просмотреть файл

@ -1,42 +0,0 @@
#!/bin/sh
set -euo pipefail
# Path to mach relative to /third_party/js/cfworker
MACH=$(realpath "../../../mach")
if [[ $(uname -a) == *MSYS* ]]; then
MACH="python ${MACH}"
fi
NODE="${MACH} node"
NPM="${MACH} npm"
# Delete empty vestigial directories.
rm -rf .changeset/ .github/ .vscode/
# Patch typescript config to ensure we have LF endings.
patch tsconfig-base.json tsconfig-base.json.patch
cd packages/json-schema
# Install dependencies.
${NPM} install --also=dev
# Compile TypeScript into JavaScript.
${NPM} run build
# Install rollup and bundle into a single module.
${NPM} install rollup@~2.67.x
${NODE} node_modules/rollup/dist/bin/rollup \
dist/index.js \
--file json-schema.js \
--format cjs
cd ../..
# Patch the CommonJS module into a regular JS file and include a copyright notice.
patch packages/json-schema/json-schema.js json-schema.js.patch
awk -f exports.awk packages/json-schema/json-schema.js >json-schema.js
# Remove source files we no longer need.
rm -rf packages/ tsconfig-base.json

20
third_party/js/cfworker/exports.awk поставляемый
Просмотреть файл

@ -1,20 +0,0 @@
#!/bin/awk -f
BEGIN {
n_exports = 0;
}
{
if ($0 ~ /^exports.*=/) {
exports[n_exports] = substr($3, 0, length($3) - 1);
n_exports++;
} else {
print;
}
}
END {
for (i = 0; i < n_exports; i++) {
print "this." exports[i] " = " exports[i] ";";
}
}

1180
third_party/js/cfworker/json-schema.js поставляемый

Разница между файлами не показана из-за своего большого размера Загрузить разницу

31
third_party/js/cfworker/json-schema.js.patch поставляемый
Просмотреть файл

@ -1,31 +0,0 @@
--- json-schema.js
+++ json-schema.js
@@ -1,6 +1,26 @@
-'use strict';
+/*
+ * Copyright (c) 2020 Jeremy Danyow
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
-Object.defineProperty(exports, '__esModule', { value: true });
+'use strict';
function deepCompareStrict(a, b) {
const typeofa = typeof a;

44
third_party/js/cfworker/moz.yaml поставляемый
Просмотреть файл

@ -1,44 +0,0 @@
schema: 1
bugzilla:
product: Toolkit
component: General
origin:
name: "cfworker"
description: A JSON schema validator
url: https://github.com/cfworker/cfworker/tree/main/packages/json-schema
license: MIT
release: commit @cfworker/dev@1.13.2 (2022-01-23T22:05:24+00:00).
revision: "v1.10.1"
vendoring:
url: https://github.com/cfworker/cfworker
source-hosting: github
tracking: tag
skip-vendoring-steps: ["update-moz-build"]
keep:
- build.sh
- exports.awk
- json-schema.jsm.patch
- tsconfig-base.json.patch
exclude:
- "**"
- ".*"
- ".changeset"
- ".github"
- ".vscode"
include:
- LICENSE.md
- packages/json-schema/src
- packages/json-schema/package.json
- packages/json-schema/tsconfig.json
- tsconfig-base.json
update-actions:
- action: run-script
script: 'build.sh'
cwd: '{yaml_dir}'

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

@ -1,9 +0,0 @@
--- tsconfig-base.json
+++ tsconfig-base.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "newLine": "lf",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,

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

@ -455,7 +455,8 @@ cfr:
hasExposure: true
exposureDescription: "Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
isEarlyStartup: false
schema: "resource://activity-stream/schemas/CFR/ExtensionDoorHanger.schema.json"
schema: >-
"browser/components/newtab/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json"
variables: {}
"moments-page":
description: "Message with URL data for Messaging System"
@ -463,7 +464,8 @@ cfr:
exposureDescription: >-
"Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
isEarlyStartup: false
schema: "resource://activity-stream/schemas/OnboardingMessage/Spotlight.schema.json"
schema: >-
"browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json"
variables: {}
infobar:
description: "Message template for Messaging System"
@ -471,7 +473,8 @@ infobar:
exposureDescription: >-
"Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
isEarlyStartup: false
schema: "resource://activity-stream/schemas/CFR/InfoBar.schema.json"
schema: >-
"browser/components/newtab/content-src/asrouter/templates/CFR/templates/InfoBar.schema.json"
variables: {}
spotlight:
description: "Modal message template for Messaging System"
@ -479,7 +482,8 @@ spotlight:
exposureDescription: >-
"Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
isEarlyStartup: false
schema: "resource://activity-stream/schemas/OnboardingMessage/Spotlight.schema.json"
schema: >-
"browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json"
variables: {}
pbNewtab:
description: Message shown on the PB newtab for Messaging System
@ -487,7 +491,8 @@ pbNewtab:
exposureDescription: >-
Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched.
isEarlyStartup: false
schema: "resource://activity-stream/schemas/PBNewtab/NewtabPromoMessage.schema.json"
schema: >-
browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json
variables: {}
syncAfterTabChange:
description: "Schedule a sync after any tab change"

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

@ -7,4 +7,3 @@ toolkit.jar:
res/nimbus/lib/ (./lib/*.jsm)
res/nimbus/ExperimentAPI.jsm (./ExperimentAPI.jsm)
res/nimbus/FeatureManifest.js (FeatureManifest.js)
res/nimbus/schemas/ (./schemas/*.schema.json)

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

@ -135,13 +135,7 @@ class _ExperimentManager {
}
}
_checkUnseenEnrollments(
enrollments,
sourceToCheck,
recipeMismatches,
invalidRecipes,
invalidBranches
) {
_checkUnseenEnrollments(enrollments, sourceToCheck, recipeMismatches) {
for (const enrollment of enrollments) {
const { slug, source } = enrollment;
if (sourceToCheck !== source) {
@ -150,16 +144,9 @@ class _ExperimentManager {
if (!this.sessions.get(source)?.has(slug)) {
log.debug(`Stopping study for recipe ${slug}`);
try {
let reason;
if (recipeMismatches.includes(slug)) {
reason = "targeting-mismatch";
} else if (invalidRecipes.includes(slug)) {
reason = "invalid-recipe";
} else if (invalidBranches.includes(slug)) {
reason = "invalid-branch";
} else {
reason = "recipe-not-seen";
}
let reason = recipeMismatches.includes(slug)
? "targeting-mismatch"
: "recipe-not-seen";
this.unenroll(slug, reason);
} catch (err) {
Cu.reportError(err);
@ -174,10 +161,7 @@ class _ExperimentManager {
* @param {string} sourceToCheck
* @param {object} options Extra context used in telemetry reporting
*/
onFinalize(
sourceToCheck,
{ recipeMismatches = [], invalidRecipes = [], invalidBranches = [] } = {}
) {
onFinalize(sourceToCheck, { recipeMismatches } = { recipeMismatches: [] }) {
if (!sourceToCheck) {
throw new Error("When calling onFinalize, you must specify a source.");
}
@ -186,16 +170,12 @@ class _ExperimentManager {
this._checkUnseenEnrollments(
activeExperiments,
sourceToCheck,
recipeMismatches,
invalidRecipes,
invalidBranches
recipeMismatches
);
this._checkUnseenEnrollments(
activeRollouts,
sourceToCheck,
recipeMismatches,
invalidRecipes,
invalidBranches
recipeMismatches
);
this.sessions.delete(sourceToCheck);
@ -264,7 +244,6 @@ class _ExperimentManager {
}
}
dump(`*** *** call this._enroll recipe.slug = ${recipe.slug}\n`);
return this._enroll(recipe, branch, source);
}
@ -303,12 +282,10 @@ class _ExperimentManager {
}
if (isRollout) {
dump(`*** *** store.addEnrollment rollout\n`);
experiment.experimentType = "rollout";
this.store.addEnrollment(experiment);
this.setExperimentActive(experiment);
} else {
dump(`*** *** store.addEnrollment non rollout\n`);
this.store.addEnrollment(experiment);
this.setExperimentActive(experiment);
}

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

@ -426,10 +426,6 @@ class ExperimentStore extends SharedDataMap {
`Tried to add an experiment but it didn't have a .slug property.`
);
}
dump(
`*** *** addEnrollment ${enrollment.slug} ${enrollment.branch.slug}\n`
);
this.set(enrollment.slug, enrollment);
this._updateSyncStore(enrollment);
this._emitUpdates(enrollment);

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

@ -14,15 +14,12 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
XPCOMUtils.defineLazyModuleGetters(this, {
ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm",
TargetingContext: "resource://messaging-system/targeting/Targeting.jsm",
ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
RemoteSettings: "resource://services-settings/remote-settings.js",
CleanupManager: "resource://normandy/lib/CleanupManager.jsm",
NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
Validator: "resource://gre/modules/JsonSchema.jsm",
});
XPCOMUtils.defineLazyGetter(this, "log", () => {
@ -63,16 +60,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
false
);
const SCHEMAS = {
get NimbusExperiment() {
return fetch("resource://nimbus/schemas/NimbusExperiment.schema.json", {
credentials: "omit",
})
.then(rsp => rsp.json())
.then(json => json.definitions.NimbusExperiment);
},
};
class _RemoteSettingsExperimentLoader {
constructor() {
// Has the timer been set?
@ -211,37 +198,11 @@ class _RemoteSettingsExperimentLoader {
Cu.reportError(e);
}
const recipeValidator = new Validator(await SCHEMAS.NimbusExperiment);
let matches = 0;
let recipeMismatches = [];
let invalidRecipes = [];
let invalidBranches = [];
let validatorCache = {};
if (recipes && !loadingError) {
for (const r of recipes) {
let validation = recipeValidator.validate(r);
if (!validation.valid) {
Cu.reportError(
`Could not validate experiment recipe ${r.id}: ${JSON.stringify(
validation.errors,
undefined,
2
)}`
);
invalidRecipes.push(r.slug);
continue;
}
let type = r.isRollout ? "rollout" : "experiment";
if (!(await this._validateBranches(r, validatorCache))) {
invalidBranches.push(r.slug);
log.debug(`${r.id} did not validate`);
continue;
}
if (await this.checkTargeting(r)) {
matches++;
log.debug(`[${type}] ${r.id} matched`);
@ -253,11 +214,7 @@ class _RemoteSettingsExperimentLoader {
}
log.debug(`${matches} recipes matched. Finalizing ExperimentManager.`);
this.manager.onFinalize("rs-loader", {
recipeMismatches,
invalidRecipes,
invalidBranches,
});
this.manager.onFinalize("rs-loader", { recipeMismatches });
}
if (trigger !== "timer") {
@ -336,67 +293,6 @@ class _RemoteSettingsExperimentLoader {
);
log.debug("Registered update timer");
}
/**
* Validate the branches of an experiment using schemas
*
* @param recipe The recipe object.
* @param validatorCache A cache of JSON Schema validators keyed by feature
* ID.
*
* @returns Whether or not the branches pass validation.
*/
async _validateBranches({ id, branches }, validatorCache = {}) {
for (const [branchIdx, branch] of branches.entries()) {
const features = branch.features ?? [branch.feature];
for (const feature of features) {
const { featureId, value } = feature;
if (!NimbusFeatures[featureId]) {
Cu.reportError(
`Experiment ${id} has unknown featureId: ${featureId}`
);
return false;
}
let validator;
if (validatorCache[featureId]) {
validator = validatorCache[featureId];
} else if (NimbusFeatures[featureId].manifest.schema) {
const schema = await fetch(
NimbusFeatures[featureId].manifest.schema,
{
credentials: "omit",
}
).then(rsp => rsp.json());
validator = validatorCache[featureId] = new Validator(schema);
} else {
// TODO: Convert NimbusFeatures[featureId].manifest.variables into a
// schema OR add schemas for all.
continue;
}
if (feature.enabled ?? true) {
const result = validator.validate(value);
if (!result.valid) {
Cu.reportError(
`Experiment ${id} branch ${branchIdx} feature ${featureId} does not validate: ${JSON.stringify(
result.errors,
undefined,
2
)}`
);
return false;
}
} else {
log.debug(
`Experiment ${id} branch ${branchIdx} feature ${featureId} disabled; skipping validation`
);
}
}
}
return true;
}
}
const RemoteSettingsExperimentLoader = new _RemoteSettingsExperimentLoader();

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

@ -29,6 +29,10 @@ XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
SPHINX_TREES["docs"] = "docs"
TESTING_JS_MODULES += [
"schemas/ExperimentFeatureManifest.schema.json",
"schemas/ExperimentFeatureRemote.schema.json",
"schemas/NimbusEnrollment.schema.json",
"schemas/NimbusExperiment.schema.json",
"test/NimbusTestUtils.jsm",
]

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

@ -1,208 +1,108 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/NimbusEnrollment",
"$ref": "#/definitions/NimbusExperiment",
"definitions": {
"NimbusEnrollment": {
"NimbusExperiment": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Unique identifier for the experiment"
},
"userFacingName": {
"type": "string",
"description": "Public name of the experiment displayed on \"about:studies\""
},
"userFacingDescription": {
"type": "string",
"description": "Short public description of the experiment displayed on on \"about:studies\""
},
"featureIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of featureIds the experiment contains configurations for."
},
"branch": {
"type": "object",
"properties": {
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"isEarlyStartup": {
"type": "boolean",
"description": "Early startup features are stored to prefs."
},
"value": {
"anyOf": [
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"feature": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"enabled": {
"type": "boolean",
"description": "This can be used to turn the whole feature on/off"
},
"value": {
"type": "object",
"additionalProperties": {},
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
},
"required": [
"featureId",
"value"
],
"description": "A single feature configuration"
}
},
"required": [
"slug",
"feature"
]
},
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"feature": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"const": "unused-feature-id-for-legacy-support"
},
"enabled": {
"type": "boolean",
"const": false
},
"value": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"featureId",
"enabled",
"value"
],
"description": "The feature key must be provided with valid values to prevent crashes if the DTO is encountered by Desktop clients earlier than version 95."
},
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"enabled": {
"type": "boolean",
"description": "This can be used to turn the whole feature on/off"
},
"value": {
"type": "object",
"additionalProperties": {},
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
},
"required": [
"featureId",
"value"
]
},
"description": "An array of feature configurations"
}
},
"required": [
"slug",
"feature",
"features"
]
},
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
},
"enabled": {
"type": "boolean",
"description": "This can be used to turn the whole feature on/off"
},
"value": {
"type": "object",
"additionalProperties": {},
"description": "Optional extra params for the feature (this should be validated against a schema)"
"description": "(deprecated)"
}
},
"required": [
"featureId",
"value"
]
},
"description": "An array of feature configurations"
"required": ["featureId", "value"],
"additionalProperties": false
}
}
},
"required": [
"slug",
"features"
]
}
],
"description": "Branch configuration for the experiment"
},
"isRollout": {
"type": "boolean",
"description": "Whether or not this experiment is considered a rollout."
},
"experimentType": {
"type": "string",
"description": "What kind of experiment this enrollment corresponds to."
},
"enrollmentId": {
"type": "string",
"description": "A unique identifier for the enrollment."
"required": ["features"]
},
"active": {
"type": "boolean",
"description": "Whether or not the enrollment is active."
"description": "Experiment status"
},
"isRollout": {
"type": "boolean",
"description": "If this is true, the enrollment is a rollout. If it is missing or false, it is an experiment."
},
"enrollmentId": {
"type": "string",
"description": "Unique identifier used in telemetry"
},
"experimentType": {
"type": "string"
},
"isEnrollmentPaused": {
"type": "boolean"
},
"source": {
"type": "string",
"description": "What triggered the enrollment"
},
"userFacingName": {
"type": "string"
},
"userFacingDescription": {
"type": "string"
},
"lastSeen": {
"type": "string",
"format": "date-time",
"description": "The last time the experiment was seen."
"description": "When was the enrollment made"
},
"force": {
"type": "boolean",
"description": "Whether or not this was a force enrollment."
"description": "(debug) If the enrollment happened naturally or through devtools"
},
"featureIds": {
"type": "array",
"items": [{ "type": "string" }],
"description": "Array of strings corresponding to the branch features in the enrollment."
}
},
"required": [
"slug",
"branch",
"active",
"enrollmentId",
"experimentType",
"source",
"userFacingName",
"userFacingDescription",
"branch",
"enrollmentId",
"active",
"lastSeen"
"featureIds"
],
"description": "An enrollment in a Nimbus Experiment saved to disk"
"additionalProperties": false,
"description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"
}
}
}

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

@ -5,29 +5,17 @@
"NimbusExperiment": {
"type": "object",
"properties": {
"schemaVersion": {
"type": "string",
"description": "Version of the NimbusExperiment schema this experiment refers to"
},
"slug": {
"type": "string",
"description": "Unique identifier for the experiment"
},
"id": {
"type": "string",
"description": "Unique identifier for the experiment. This is a duplicate of slug, but is a required field for all Remote Settings records."
"description": "Unique identifier for the experiment. This is a duplicate of slug, but is a required field\nfor all Remote Settings records."
},
"appName": {
"application": {
"type": "string",
"description": "A slug identifying the targeted product for this experiment. It should be a lowercase_with_underscores name that is short and unambiguous and it should match the app_name found in https://probeinfo.telemetry.mozilla.org/glean/repositories. Examples are \"fenix\" or \"firefox_desktop\"."
},
"appId": {
"type": "string",
"description": "The platform identifier for the targeted app. The app's identifier exactly as it appears in the relevant app store listing (for relevant platforms) or in the app's Glean initialization call (for other platforms). Examples are \"org.mozilla.firefox_beta\" or \"firefox-desktop\"."
},
"channel": {
"type": "string",
"description": "A specific channel of an application such as \"nightly\", \"beta\", or \"release\""
"description": "A specific product such as Firefox Desktop or Fenix that supports Nimbus experiments"
},
"userFacingName": {
"type": "string",
@ -39,7 +27,7 @@
},
"isEnrollmentPaused": {
"type": "boolean",
"description": "When this property is set to true, the the SDK should not enroll new users into the experiment that have not already been enrolled."
"description": "Should we enroll new users into the experiment?"
},
"bucketConfig": {
"type": "object",
@ -53,16 +41,16 @@
"description": "Additional inputs to the hashing function"
},
"start": {
"type": "integer",
"type": "number",
"description": "Index of start of the range of buckets"
},
"count": {
"type": "integer",
"type": "number",
"description": "Number of buckets to check"
},
"total": {
"type": "integer",
"description": "Total number of buckets. You can assume this will always be 10000.",
"type": "number",
"description": "Total number of buckets",
"default": 10000
}
},
@ -73,267 +61,116 @@
"count",
"total"
],
"additionalProperties": false,
"description": "Bucketing configuration"
},
"outcomes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the outcome"
},
"priority": {
"type": "string",
"description": "e.g. \"primary\" or \"secondary\""
}
},
"required": [
"slug",
"priority"
]
},
"description": "A list of outcomes relevant to the experiment analysis."
},
"featureIds": {
"probeSets": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of featureIds the experiment contains configurations for."
"description": "A list of probe set slugs relevant to the experiment analysis"
},
"branches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"ratio": {
"type": "number",
"description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3,\nbranch A would get 25% of the population)",
"default": 1
},
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"value": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"ratio": {
"type": "integer",
"description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3, branch A would get 25% of the population)",
"default": 1
},
"feature": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"enabled": {
"type": "boolean",
"description": "This can be used to turn the whole feature on/off"
},
"value": {
"type": "object",
"additionalProperties": {},
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
},
"required": [
"featureId",
"value"
],
"description": "A single feature configuration"
}
},
"required": [
"slug",
"ratio",
"feature"
]
}
},
{
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"ratio": {
"type": "integer",
"description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3, branch A would get 25% of the population)",
"default": 1
},
"feature": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"const": "unused-feature-id-for-legacy-support"
},
"enabled": {
"type": "boolean",
"const": false
},
"value": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"featureId",
"enabled",
"value"
],
"description": "The feature key must be provided with valid values to prevent crashes if the DTO is encountered by Desktop clients earlier than version 95."
},
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"enabled": {
"type": "boolean",
"description": "This can be used to turn the whole feature on/off"
},
"value": {
"type": "object",
"additionalProperties": {},
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
},
"required": [
"featureId",
"value"
]
},
"description": "An array of feature configurations"
}
},
"required": [
"slug",
"ratio",
"feature",
"features"
]
}
},
{
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier for the branch"
},
"ratio": {
"type": "integer",
"description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3, branch A would get 25% of the population)",
"default": 1
},
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"enabled": {
"type": "boolean",
"description": "This can be used to turn the whole feature on/off"
},
"value": {
"type": "object",
"additionalProperties": {},
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
},
"required": [
"featureId",
"value"
]
},
"description": "An array of feature configurations"
"required": ["featureId", "value"],
"additionalProperties": false
}
}
},
"required": [
"slug",
"ratio",
"features"
]
}
}
],
"required": ["slug", "ratio"],
"additionalProperties": false
},
"description": "Branch configuration for the experiment"
},
"targeting": {
"type": [
"string",
"null"
],
"type": "string",
"description": "JEXL expression used to filter experiments based on locale, geo, etc."
},
"startDate": {
"type": [
"string",
"null"
],
"description": "Actual publish date of the experiment Note that this value is expected to be null in Remote Settings.",
"format": "date"
"type": ["string", "null"],
"description": "Actual publish date of the experiment\nNote that this value is expected to be null in Remote Settings.",
"format": "date-time"
},
"endDate": {
"type": [
"string",
"null"
],
"description": "Actual end date of the experiment. Note that this value is expected to be null in Remote Settings.",
"format": "date"
"type": ["string", "null"],
"description": "Actual end date of the experiment.\nNote that this value is expected to be null in Remote Settings.",
"format": "date-time"
},
"proposedDuration": {
"type": "integer",
"description": "Duration of the experiment from the start date in days. Note that this property is only used during the analysis phase (not by the SDK)"
"type": "number",
"description": "Duration of the experiment from the start date in days.\nNote that this value is expected to be null in Remote Settings.\nin Remote Settings."
},
"proposedEnrollment": {
"type": "integer",
"description": "This represents the number of days that we expect to enroll new users. Note that this property is only used during the analysis phase (not by the SDK)"
"type": "number",
"description": "Duration of enrollment from the start date in days"
},
"referenceBranch": {
"type": [
"string",
"null"
],
"description": "The slug of the reference branch (that is, which branch we consider \"control\")"
"type": ["string", "null"],
"description": "The slug of the reference branch"
},
"filter_expression": {
"type": "string",
"description": "This is NOT used by Nimbus, but has special functionality in Remote Settings. See https://remote-settings.readthedocs.io/en/latest/target-filters.html#how"
"description": "This is NOT used by Nimbus, but has special functionality in Remote Settings.\nSee https://remote-settings.readthedocs.io/en/latest/target-filters.html#how"
},
"featureIds": {
"type": "array",
"items": [{ "type": "string" }],
"description": "Array of strings corresponding to the branch features in the enrollment."
}
},
"required": [
"schemaVersion",
"slug",
"id",
"appName",
"appId",
"channel",
"application",
"userFacingName",
"userFacingDescription",
"isEnrollmentPaused",
"bucketConfig",
"probeSets",
"branches",
"startDate",
"endDate",
"proposedEnrollment",
"referenceBranch"
"referenceBranch",
"featureIds"
],
"description": "The experiment definition accessible to: 1. The Nimbus SDK via Remote Settings 2. Jetstream via the Experimenter API"
"additionalProperties": true,
"description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"
}
}
}

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

@ -15,7 +15,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
_ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
ExperimentStore: "resource://nimbus/lib/ExperimentStore.jsm",
NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
NormandyUtils: "resource://normandy/lib/NormandyUtils.jsm",
FileTestUtils: "resource://testing-common/FileTestUtils.jsm",
_RemoteSettingsExperimentLoader:
@ -85,7 +84,7 @@ const ExperimentTestUtils = {
async validateExperiment(experiment) {
const schema = (
await fetchSchema(
"resource://nimbus/schemas/NimbusExperiment.schema.json"
"resource://testing-common/NimbusExperiment.schema.json"
)
).NimbusExperiment;
@ -110,9 +109,9 @@ const ExperimentTestUtils = {
async validateEnrollment(enrollment) {
const schema = (
await fetchSchema(
"resource://nimbus/schemas/NimbusEnrollment.schema.json"
"resource://testing-common/NimbusEnrollment.schema.json"
)
).NimbusEnrollment;
).NimbusExperiment;
// We still have single feature experiment recipes for backwards
// compatibility testing but we don't do schema validation
@ -132,9 +131,9 @@ const ExperimentTestUtils = {
async validateRollouts(rollout) {
const schema = (
await fetchSchema(
"resource://nimbus/schemas/NimbusEnrollment.schema.json"
"resource://testing-common/NimbusEnrollment.schema.json"
)
).NimbusEnrollment;
).NimbusExperiment;
return this._validator(
schema,
@ -142,26 +141,6 @@ const ExperimentTestUtils = {
`Rollout configuration ${rollout.slug} is not valid`
);
},
/**
* Add features for tests.
*
* These features will only be visible to the JS Nimbus client. The native
* Nimbus client will have no access.
*
* @params features A list of |_NimbusFeature|s.
*
* @returns A cleanup function to remove the features once the test has completed.
*/
addTestFeatures(...features) {
for (const feature of features) {
NimbusFeatures[feature.featureId] = feature;
}
return () => {
for (const { featureId } of features) {
delete NimbusFeatures[featureId];
}
};
},
};
const ExperimentFakes = {
@ -330,8 +309,8 @@ const ExperimentFakes = {
slug: "treatment",
features: [
{
featureId: "testFeature",
value: { testInt: 123, enabled: true },
featureId: "test-feature",
value: { title: "hello", enabled: true },
},
],
...props,
@ -341,9 +320,8 @@ const ExperimentFakes = {
experimentType: "NimbusTestUtils",
userFacingName: "NimbusTestUtils",
userFacingDescription: "NimbusTestUtils",
lastSeen: new Date().toJSON(),
featureIds: props?.branch?.features?.map(f => f.featureId) || [
"testFeature",
"test-feature",
],
...props,
};
@ -358,8 +336,8 @@ const ExperimentFakes = {
slug: "treatment",
features: [
{
featureId: "testFeature",
value: { testInt: 123, enabled: true },
featureId: "test-feature",
value: { title: "hello", enabled: true },
},
],
...props,
@ -369,10 +347,9 @@ const ExperimentFakes = {
experimentType: "rollout",
userFacingName: "NimbusTestUtils",
userFacingDescription: "NimbusTestUtils",
lastSeen: new Date().toJSON(),
featureIds: (props?.branch?.features || props?.features)?.map(
f => f.featureId
) || ["testFeature"],
) || ["test-feature"],
...props,
};
},
@ -380,10 +357,6 @@ const ExperimentFakes = {
return {
// This field is required for populating remote settings
id: NormandyUtils.generateUuid(),
schemaVersion: "1.7.0",
appName: "firefox_desktop",
appId: "firefox-desktop",
channel: "nightly",
slug,
isEnrollmentPaused: false,
probeSets: [],
@ -396,20 +369,15 @@ const ExperimentFakes = {
{
slug: "control",
ratio: 1,
features: [
{
featureId: "testFeature",
value: { testInt: 123, enabled: true },
},
],
features: [{ featureId: "test-feature", value: { enabled: true } }],
},
{
slug: "treatment",
ratio: 1,
features: [
{
featureId: "testFeature",
value: { testInt: 123, enabled: true },
featureId: "test-feature",
value: { title: "hello", enabled: true },
},
],
},
@ -424,7 +392,7 @@ const ExperimentFakes = {
userFacingName: "Nimbus recipe",
userFacingDescription: "NimbusTestUtils recipe",
featureIds: props?.branches?.[0].features?.map(f => f.featureId) || [
"testFeature",
"test-feature",
],
...props,
};

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

@ -10,7 +10,7 @@ const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
add_setup(async function setup() {
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
["messaging-system.log", "all"],
@ -79,15 +79,25 @@ add_task(async function test_evaluate_active_experiments_activeExperiments() {
const slug = "foo" + Math.random();
// Init the store before we use it
await ExperimentManager.onStartup();
let recipe = ExperimentFakes.recipe(slug);
recipe.branches[0].slug = "mochitest-active-foo";
delete recipe.branches[1];
let {
enrollmentPromise,
doExperimentCleanup,
} = ExperimentFakes.enrollmentHelper(recipe);
} = ExperimentFakes.enrollmentHelper(
ExperimentFakes.recipe(slug, {
branches: [
{
slug: "mochitest-active-foo",
features: [
{
enabled: true,
featureId: "foo",
value: null,
},
],
},
],
})
);
await enrollmentPromise;

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

@ -13,6 +13,7 @@ const { ExperimentAPI, NimbusFeatures } = ChromeUtils.import(
const SINGLE_FEATURE_RECIPE = {
appId: "firefox-desktop",
appName: "firefox_desktop",
application: "firefox-desktop",
arguments: {},
branches: [
{

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

@ -11,9 +11,6 @@ const {
NimbusFeatures,
ExperimentAPI,
} = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
const { ExperimentTestUtils } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { ExperimentManager } = ChromeUtils.import(
"resource://nimbus/lib/ExperimentManager.jsm"
);
@ -67,7 +64,6 @@ const REMOTE_CONFIGURATION_FOO = ExperimentFakes.recipe("foo-rollout", {
branches: [
{
slug: "foo-rollout-branch",
ratio: 1,
features: [
{
featureId: "foo",
@ -85,7 +81,6 @@ const REMOTE_CONFIGURATION_BAR = ExperimentFakes.recipe("bar-rollout", {
branches: [
{
slug: "bar-rollout-branch",
ratio: 1,
features: [
{
featureId: "bar",
@ -132,10 +127,6 @@ add_task(async function test_remote_fetch_and_ready() {
"setExperimentInactive"
);
registerCleanupFunction(
ExperimentTestUtils.addTestFeatures(fooInstance, barInstance)
);
Assert.equal(
fooInstance.getVariable("remoteValue"),
undefined,
@ -301,11 +292,6 @@ add_task(async function test_finalizeRemoteConfigs_cleanup() {
const featureBar = new ExperimentFeature("bar", {
foo: { description: "mochitests" },
});
registerCleanupFunction(
ExperimentTestUtils.addTestFeatures(featureFoo, featureBar)
);
let fooCleanup = await ExperimentFakes.enrollWithRollout(
{
featureId: "foo",

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

@ -165,12 +165,8 @@ add_task(async function test_getExperiment_feature() {
const expected = ExperimentFakes.experiment("foo", {
branch: {
slug: "treatment",
value: { title: "hi" },
features: [{ featureId: "cfr", enabled: true, value: null }],
feature: {
featureId: "unused-feature-id-for-legacy-support",
enabled: false,
value: {},
},
},
});
@ -332,8 +328,8 @@ add_task(async function test_getAllBranches_featureIdAccessor() {
);
branches.forEach(branch => {
Assert.equal(
branch.testFeature.featureId,
"testFeature",
branch["test-feature"].featureId,
"test-feature",
"Should use the experimentBranchAccessor proxy getter"
);
});

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

@ -100,7 +100,6 @@ add_task(async function test_ExperimentFeature_isEnabled_default_over_remote() {
const { manager, sandbox } = await setupForExperimentFeature();
const rollout = ExperimentFakes.rollout("foo-rollout", {
branch: {
slug: "slug",
features: [
{
featureId: "foo",
@ -455,31 +454,15 @@ add_task(async function test_onUpdate_before_store_ready() {
add_task(async function test_ExperimentFeature_test_ready_late() {
const { manager, sandbox } = await setupForExperimentFeature();
const stub = sandbox.stub();
sandbox
.stub(manager.store, "getAllRollouts")
.returns([ExperimentFakes.rollout("foo")]);
await manager.onStartup();
const featureInstance = new ExperimentFeature(
"test-feature",
FAKE_FEATURE_MANIFEST
);
const rollout = ExperimentFakes.rollout("foo", {
branch: {
slug: "slug",
features: [
{
featureId: featureInstance.featureId,
value: {
title: "hello",
enabled: true,
},
},
],
},
});
sandbox.stub(manager.store, "getAllRollouts").returns([rollout]);
await manager.onStartup();
featureInstance.onUpdate(stub);
await featureInstance.ready();

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

@ -91,7 +91,6 @@ add_task(async function test_ExperimentFeature_getVariable_precedence() {
const prefName = TEST_VARIABLES.items.fallbackPref;
const rollout = ExperimentFakes.rollout(`${FEATURE_ID}-rollout`, {
branch: {
slug: "slug",
features: [
{
featureId: FEATURE_ID,
@ -166,7 +165,6 @@ add_task(async function test_ExperimentFeature_getVariable_partial_values() {
const instance = createInstanceWithVariables(TEST_VARIABLES);
const rollout = ExperimentFakes.rollout(`${FEATURE_ID}-rollout`, {
branch: {
slug: "slug",
features: [
{
featureId: FEATURE_ID,

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

@ -7,16 +7,20 @@ const {
Cu.importGlobalProperties(["fetch"]);
XPCOMUtils.defineLazyGetter(this, "fetchSchema", () => {
return fetch("resource://nimbus/schemas/NimbusEnrollment.schema.json", {
credentials: "omit",
}).then(rsp => rsp.json());
XPCOMUtils.defineLazyGetter(this, "fetchSchema", async () => {
const response = await fetch(
"resource://testing-common/NimbusEnrollment.schema.json"
);
const schema = await response.json();
if (!schema) {
throw new Error("Failed to load ExperimentFeatureRemote schema");
}
return schema.definitions.NimbusExperiment;
});
const NON_MATCHING_ROLLOUT = Object.freeze(
ExperimentFakes.rollout("non-matching-rollout", {
branch: {
slug: "slug",
features: [
{
featureId: "aboutwelcome",
@ -29,7 +33,6 @@ const NON_MATCHING_ROLLOUT = Object.freeze(
const MATCHING_ROLLOUT = Object.freeze(
ExperimentFakes.rollout("matching-rollout", {
branch: {
slug: "slug",
features: [
{
featureId: "aboutwelcome",
@ -149,7 +152,6 @@ add_task(async function test_features_over_feature() {
const rollout_features_and_feature = Object.freeze(
ExperimentFakes.rollout("matching-rollout", {
branch: {
slug: "slug",
feature: {
featureId: "aboutwelcome",
value: { enabled: false },
@ -166,7 +168,6 @@ add_task(async function test_features_over_feature() {
const rollout_just_feature = Object.freeze(
ExperimentFakes.rollout("matching-rollout", {
branch: {
slug: "slug",
feature: {
featureId: "aboutwelcome",
value: { enabled: false },

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

@ -58,8 +58,6 @@ add_task(async function test_add_to_store() {
NormandyTestUtils.isUuid(experiment.enrollmentId),
"should add a valid enrollmentId"
);
manager.unenroll("foo", "test-cleanup");
});
add_task(async function test_add_rollout_to_store() {
@ -93,8 +91,6 @@ add_task(async function test_add_rollout_to_store() {
"should choose a branch from the recipe.branches"
);
Assert.equal(experiment.isRollout, true, "should have .isRollout");
manager.unenroll("rollout-slug", "test-cleanup");
});
add_task(
@ -129,8 +125,6 @@ add_task(
true,
"should call sendEnrollmentTelemetry after an enrollment"
);
manager.unenroll("foo", "test-cleanup");
}
);
@ -201,16 +195,14 @@ add_task(async function test_setRolloutActive_sendEnrollmentTelemetry_called() {
"Should send telemetry with expected values"
);
manager.unenroll("rollout", "test-cleanup");
globalSandbox.restore();
});
// /**
// * Failure cases:
// * - slug conflict
// * - group conflict
// */
/**
* Failure cases:
* - slug conflict
* - group conflict
*/
add_task(async function test_failure_name_conflict() {
const manager = ExperimentFakes.manager();
@ -237,8 +229,6 @@ add_task(async function test_failure_name_conflict() {
true,
"should send failure telemetry if a conflicting experiment exists"
);
manager.unenroll("foo", "test-cleanup");
});
add_task(async function test_failure_group_conflict() {
@ -286,8 +276,6 @@ add_task(async function test_failure_group_conflict() {
true,
"should send failure telemetry if a feature conflict exists"
);
manager.unenroll("foo", "test-cleanup");
});
add_task(async function test_rollout_failure_group_conflict() {
@ -321,8 +309,6 @@ add_task(async function test_rollout_failure_group_conflict() {
true,
"should send failure telemetry if a feature conflict exists"
);
manager.unenroll("rollout-enrollment", "test-cleanup");
});
add_task(async function test_rollout_experiment_no_conflict() {
@ -350,8 +336,6 @@ add_task(async function test_rollout_experiment_no_conflict() {
manager.sendFailureTelemetry.notCalled,
"Should send failure telemetry if a feature conflict exists"
);
manager.unenroll("rollout-enrollment", "test-cleanup");
});
add_task(async function test_sampling_check() {
@ -508,8 +492,6 @@ add_task(async function test_forceEnroll_cleanup() {
"Enrolled in forced experiment"
);
manager.unenroll(`optin-${forcedRecipe.slug}`, "test-cleanup");
sandbox.restore();
});
@ -567,7 +549,6 @@ add_task(async function test_featuremanifest_enum() {
"Enrollment was validated and stored"
);
manager.unenroll(recipe.slug, "test-cleanup");
manager = ExperimentFakes.manager();
await manager.onStartup();
@ -601,28 +582,20 @@ add_task(async function test_featureIds_is_stored() {
const recipe = ExperimentFakes.recipe("featureIds");
// Ensure we get enrolled
recipe.bucketConfig.count = recipe.bucketConfig.total;
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
const manager = ExperimentFakes.manager();
await manager.onStartup();
const {
enrollmentPromise,
doExperimentCleanup,
} = ExperimentFakes.enrollmentHelper(recipe, { manager });
await enrollmentPromise;
await manager.enroll(recipe, "test_featureIds_is_stored");
Assert.ok(manager.store.addEnrollment.calledOnce, "experiment is stored");
let [enrollment] = manager.store.addEnrollment.firstCall.args;
Assert.ok("featureIds" in enrollment, "featureIds is stored");
Assert.deepEqual(
enrollment.featureIds,
["testFeature"],
["test-feature"],
"Has expected value"
);
await doExperimentCleanup();
});
add_task(async function experiment_and_rollout_enroll_and_cleanup() {

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

@ -15,10 +15,6 @@ add_task(async function test_onStartup_setExperimentActive_called() {
sandbox.stub(manager, "setExperimentActive");
sandbox.stub(manager.store, "init").resolves();
sandbox.stub(manager.store, "getAll").returns(experiments);
sandbox
.stub(manager.store, "get")
.callsFake(slug => experiments.find(expt => expt.slug === slug));
sandbox.stub(manager.store, "set");
const active = ["foo", "bar"].map(ExperimentFakes.experiment);
@ -55,10 +51,6 @@ add_task(async function test_onStartup_setRolloutActive_called() {
const active = ["foo", "bar"].map(ExperimentFakes.rollout);
sandbox.stub(manager.store, "getAll").returns(active);
sandbox
.stub(manager.store, "get")
.callsFake(slug => active.find(e => e.slug === slug));
sandbox.stub(manager.store, "set");
await manager.onStartup();
@ -90,16 +82,16 @@ add_task(async function test_startup_unenroll() {
await enrollmentPromise;
const manager = ExperimentFakes.manager(store);
const unenrollSpy = sandbox.spy(manager, "unenroll");
const unenrollStub = sandbox.stub(manager, "unenroll");
await manager.onStartup();
Assert.ok(
unenrollSpy.calledOnce,
unenrollStub.calledOnce,
"Unenrolled from active experiment if user opt out is true"
);
Assert.ok(
unenrollSpy.calledWith("startup_unenroll", "studies-opt-out"),
unenrollStub.calledWith("startup_unenroll", "studies-opt-out"),
"Called unenroll for expected recipe"
);
@ -158,8 +150,6 @@ add_task(async function test_onRecipe_enroll() {
true,
"should add recipe to the store"
);
manager.unenroll(fooRecipe.slug, "test-cleanup");
});
add_task(async function test_onRecipe_update() {
@ -187,8 +177,6 @@ add_task(async function test_onRecipe_update() {
true,
"should call .updateEnrollment() if the recipe has already been enrolled"
);
manager.unenroll(fooRecipe.slug, "test-cleanup");
});
add_task(async function test_onRecipe_rollout_update() {
@ -311,7 +299,7 @@ add_task(async function test_onFinalize_unenroll() {
experimentType: "unittest",
userFacingName: "foo",
userFacingDescription: "foo",
lastSeen: new Date().toJSON(),
lastSeen: Date.now().toLocaleString(),
source: "test",
});
await manager.store.addEnrollment(recipe0);
@ -359,7 +347,7 @@ add_task(async function test_onFinalize_unenroll_mismatch() {
experimentType: "unittest",
userFacingName: "foo",
userFacingDescription: "foo",
lastSeen: new Date().toJSON(),
lastSeen: Date.now().toLocaleString(),
source: "test",
});
await manager.store.addEnrollment(recipe0);

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

@ -156,8 +156,6 @@ add_task(async function test_updateRecipes_someMismatch() {
ok(
loader.manager.onFinalize.calledWith("rs-loader", {
recipeMismatches: [FAIL_FILTER_RECIPE.slug],
invalidRecipes: [],
invalidBranches: [],
}),
"should call .onFinalize with the recipes that failed targeting"
);

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

@ -6,9 +6,6 @@ const { ExperimentFakes } = ChromeUtils.import(
const { FirstStartup } = ChromeUtils.import(
"resource://gre/modules/FirstStartup.jsm"
);
const { PanelTestProvider } = ChromeUtils.import(
"resource://activity-stream/lib/PanelTestProvider.jsm"
);
add_task(async function test_updateRecipes_activeExperiments() {
const manager = ExperimentFakes.manager();
@ -47,271 +44,3 @@ add_task(async function test_updateRecipes_isFirstRun() {
Assert.ok(onRecipe.calledOnce, "Should match first run");
});
add_task(async function test_updateRecipes_invalidFeatureId() {
const manager = ExperimentFakes.manager();
const sandbox = sinon.createSandbox();
const loader = ExperimentFakes.rsLoader();
loader.manager = manager;
const badRecipe = ExperimentFakes.recipe("foo", {
branches: [
{
slug: "control",
ratio: 1,
features: [
{
featureId: "invalid-feature-id",
value: { hello: "world" },
},
],
},
{
slug: "treatment",
ratio: 1,
features: [
{
featureId: "invalid-feature-id",
value: { hello: "goodbye" },
},
],
},
],
});
const onRecipe = sandbox.stub(manager, "onRecipe");
sinon.stub(loader.remoteSettingsClient, "get").resolves([badRecipe]);
sandbox.stub(manager.store, "ready").resolves();
sandbox.stub(manager.store, "getAllActive").returns([]);
await loader.init();
ok(onRecipe.notCalled, "No recipes");
});
add_task(async function test_updateRecipes_invalidFeatureValue() {
const manager = ExperimentFakes.manager();
const sandbox = sinon.createSandbox();
const loader = ExperimentFakes.rsLoader();
loader.manager = manager;
const badRecipe = ExperimentFakes.recipe("foo", {
branches: [
{
slug: "control",
ratio: 1,
features: [
{
featureId: "spotlight",
enabled: true,
value: {
id: "test-spotlight-invalid-1",
},
},
],
},
{
slug: "treatment",
ratio: 1,
features: [
{
featureId: "spotlight",
enabled: true,
value: {
id: "test-spotlight-invalid-2",
},
},
],
},
],
});
const onRecipe = sandbox.stub(manager, "onRecipe");
sinon.stub(loader.remoteSettingsClient, "get").resolves([badRecipe]);
sandbox.stub(manager.store, "ready").resolves();
sandbox.stub(manager.store, "getAllActive").returns([]);
await loader.init();
ok(onRecipe.notCalled, "No recipes");
});
add_task(async function test_updateRecipes_invalidRecipe() {
const manager = ExperimentFakes.manager();
const sandbox = sinon.createSandbox();
const loader = ExperimentFakes.rsLoader();
loader.manager = manager;
const badRecipe = ExperimentFakes.recipe("foo");
delete badRecipe.slug;
const onRecipe = sandbox.stub(manager, "onRecipe");
sinon.stub(loader.remoteSettingsClient, "get").resolves([badRecipe]);
sandbox.stub(manager.store, "ready").resolves();
sandbox.stub(manager.store, "getAllActive").returns([]);
await loader.init();
ok(onRecipe.notCalled, "No recipes");
});
add_task(async function test_updateRecipes_invalidRecipeAfterUpdate() {
const manager = ExperimentFakes.manager();
const loader = ExperimentFakes.rsLoader();
loader.manager = manager;
const recipe = ExperimentFakes.recipe("foo");
const badRecipe = { ...recipe };
delete badRecipe.branches;
sinon.stub(loader, "setTimer");
sinon.stub(manager, "onRecipe");
sinon.stub(manager, "onFinalize");
sinon.stub(loader.remoteSettingsClient, "get").resolves([recipe]);
sinon.stub(manager.store, "ready").resolves();
sinon.spy(loader, "updateRecipes");
await loader.init();
ok(loader.updateRecipes.calledOnce, "should call .updateRecipes");
equal(loader.manager.onRecipe.callCount, 1, "should call .onRecipe once");
ok(
loader.manager.onRecipe.calledWith(recipe, "rs-loader"),
"should call .onRecipe with argument data"
);
equal(loader.manager.onFinalize.callCount, 1, "should call .onFinalize once");
ok(
loader.manager.onFinalize.calledWith("rs-loader", {
recipeMismatches: [],
invalidRecipes: [],
invalidBranches: [],
}),
"should call .onFinalize with no mismatches or invalid recipes"
);
info("Replacing recipe with an invalid one");
loader.remoteSettingsClient.get.resolves([badRecipe]);
await loader.updateRecipes("timer");
equal(
loader.manager.onRecipe.callCount,
1,
"should not have called .onRecipe again"
);
equal(
loader.manager.onFinalize.callCount,
2,
"should have called .onFinalize again"
);
ok(
loader.manager.onFinalize.secondCall.calledWith("rs-loader", {
recipeMismatches: [],
invalidRecipes: ["foo"],
invalidBranches: [],
}),
"should call .onFinalize with an invalid recipe"
);
});
add_task(async function test_updateRecipes_invalidBranchAfterUpdate() {
const message = await PanelTestProvider.getMessages().then(msgs =>
msgs.find(m => m.id === "SPOTLIGHT_MESSAGE_93")
);
const manager = ExperimentFakes.manager();
const loader = ExperimentFakes.rsLoader();
loader.manager = manager;
const recipe = ExperimentFakes.recipe("foo", {
branches: [
{
slug: "control",
ratio: 1,
features: [
{
featureId: "spotlight",
value: { ...message },
},
],
},
{
slug: "treatment",
ratio: 1,
features: [
{
featureId: "spotlight",
value: { ...message },
},
],
},
],
});
const badRecipe = {
...recipe,
branches: [
{ ...recipe.branches[0] },
{
...recipe.branches[1],
features: [
{
...recipe.branches[1].features[0],
value: { ...message },
},
],
},
],
};
delete badRecipe.branches[1].features[0].value.template;
sinon.stub(loader, "setTimer");
sinon.stub(manager, "onRecipe");
sinon.stub(manager, "onFinalize");
sinon.stub(loader.remoteSettingsClient, "get").resolves([recipe]);
sinon.stub(manager.store, "ready").resolves();
sinon.spy(loader, "updateRecipes");
await loader.init();
ok(loader.updateRecipes.calledOnce, "should call .updateRecipes");
equal(loader.manager.onRecipe.callCount, 1, "should call .onRecipe once");
ok(
loader.manager.onRecipe.calledWith(recipe, "rs-loader"),
"should call .onRecipe with argument data"
);
equal(loader.manager.onFinalize.callCount, 1, "should call .onFinalize once");
ok(
loader.manager.onFinalize.calledWith("rs-loader", {
recipeMismatches: [],
invalidRecipes: [],
invalidBranches: [],
}),
"should call .onFinalize with no mismatches or invalid recipes"
);
info("Replacing recipe with an invalid one");
loader.remoteSettingsClient.get.resolves([badRecipe]);
await loader.updateRecipes("timer");
equal(
loader.manager.onRecipe.callCount,
1,
"should not have called .onRecipe again"
);
equal(
loader.manager.onFinalize.callCount,
2,
"should have called .onFinalize again"
);
ok(
loader.manager.onFinalize.secondCall.calledWith("rs-loader", {
recipeMismatches: [],
invalidRecipes: [],
invalidBranches: ["foo"],
}),
"should call .onFinalize with an invalid branch"
);
});

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

@ -115,4 +115,3 @@ toolkit.jar:
# Third party files
content/global/third_party/d3/d3.js (/third_party/js/d3/d3.js)
content/global/third_party/cfworker/json-schema.js (/third_party/js/cfworker/json-schema.js)

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

@ -77,7 +77,6 @@
<li><a href="about:license#bspatch">bspatch License</a></li>
<li><a href="about:license#byteorder">byteorder License</a></li>
<li><a href="about:license#cairo">Cairo Component Licenses</a></li>
<li><a href="about:license#cfworker">cfworker License</a></li>
<li><a href="about:license#chromium">Chromium License</a></li>
<li><a href="about:license#codemirror">CodeMirror License</a></li>
<li><a href="about:license#cryptogams">CRYPTOGAMS License</a></li>
@ -2351,35 +2350,6 @@ WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</pre>
<hr>
<h1><a id="cfworker"></a>cfworker License</h1>
<p>This license applies to the file <code>toolkit/content/third_party/cfworker/json-schema.js</code></p>
<pre>
MIT License
Copyright (c) 2020 Jeremy Danyow
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</pre>
<hr>
<h1><a id="chromium"></a>Chromium License</h1>

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

@ -1,122 +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/. */
"use strict";
/**
* A facade around @cfworker/json-schema that provides additional formats and
* convenience methods whil executing inside a sandbox.
*/
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const sandbox = new Cu.Sandbox(null, {
wantComponents: false,
wantGlobalProperties: ["URL"],
});
Services.scriptloader.loadSubScript(
"chrome://global/content/third_party/cfworker/json-schema.js",
sandbox
);
/**
* A JSON Schema string format for URLs intended to go through Services.urlFormatter.
*/
Cu.exportFunction(
function validateMozUrlFormat(input) {
try {
const formatted = Services.urlFormatter.formatURL(input);
return Cu.waiveXrays(sandbox.fastFormat).uri(formatted);
} catch {
return false;
}
},
sandbox.fastFormat,
{ defineAs: "moz-url-format" }
);
// initialBaseURI defaults to github.com/cfworker, which will be confusing.
Cu.evalInSandbox(
`this.initialBaseURI = initialBaseURI = new URL("http://mozilla.org");`,
sandbox
);
/**
* A JSONSchema validator that performs validation inside a sandbox.
*/
class Validator {
#inner;
/**
* Create a new validator.
*
* @param {object} schema The schema to validate with.
* @param {string} draft The draft to validate against. Should
* be one of "4", "7", "2019-09".
* @param {boolean} shortCircuit Whether or not the validator should return
* after a single error occurs.
*/
constructor(schema, draft = "2019-09", shortCircuit = true) {
this.#inner = Cu.waiveXrays(
new sandbox.Validator(Cu.cloneInto(schema, sandbox), draft, shortCircuit)
);
}
/**
* Validate the instance against the known schemas.
*
* @param {object} instance The instance to validate.
*
* @return {object} An object with |valid| and |errors| keys that indicates
* the success of validation.
*/
validate(instance) {
return this.#inner.validate(Cu.cloneInto(instance, sandbox));
}
/**
* Add a schema to the validator.
*
* @param {object} schema A JSON schema object.
* @param {string} id An optional ID to identify the schema if it does not
* provide an |$id| field.
*/
addSchema(schema, id) {
this.#inner.addSchema(Cu.cloneInto(schema, sandbox), id);
}
}
/**
* A wrapper around validate that provides some options as an object
* instead of positional arguments.
*
* @param {object} instance The instance to validate.
* @param {object} schema The JSON schema to validate against.
* @param {object} options Options for the validator.
* @param {string} options.draft The draft to validate against. Should
* be one of "4", "7", "2019-09".
* @param {boolean} options.shortCircuit Whether or not the validator should
* return after a single error occurs.
*
* @returns {object} An object with |valid| and |errors| keys that indicates the
* success of validation.
*/
function validate(
instance,
schema,
{ draft = "2019-09", shortCircuit = true } = {}
) {
const clonedSchema = Cu.cloneInto(schema, sandbox);
return sandbox.validate(
Cu.cloneInto(instance, sandbox),
clonedSchema,
draft,
sandbox.dereference(clonedSchema),
shortCircuit
);
}
const EXPORTED_SYMBOLS = ["Validator", "validate", "sandbox"];

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

@ -183,7 +183,6 @@ EXTRA_JS_MODULES += [
"InlineSpellCheckerContent.jsm",
"Integration.jsm",
"JSONFile.jsm",
"JsonSchema.jsm",
"KeywordUtils.jsm",
"LayoutUtils.jsm",
"Log.jsm",

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

@ -1,86 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const JsonSchema = ChromeUtils.import("resource://gre/modules/JsonSchema.jsm");
add_task(function test_basicSchema() {
info("Testing validation of a basic schema");
const schema = {
type: "object",
properties: {
id: { type: "number" },
},
required: ["id"],
};
const validator = new JsonSchema.Validator(schema);
Assert.deepEqual(
JsonSchema.validate({ id: 123 }, schema),
{ valid: true, errors: [] },
"Validation of basic schemas with validate()"
);
Assert.deepEqual(
validator.validate({ id: 123 }, schema),
{ valid: true, errors: [] },
"Validation of basic schemas with Validator"
);
Assert.ok(
!JsonSchema.validate({}, schema).valid,
"Validation of basic schemas with validate()"
);
Assert.ok(
!validator.validate({}).valid,
"Validation of basic schemas with Validator"
);
});
add_task(function test_mozUrlFormat() {
info("Testing custom string format 'moz-url-format'");
const schema = {
type: "string",
format: "moz-url-format",
};
{
const obj = "https://www.mozilla.org/%LOCALE%/";
Assert.deepEqual(
JsonSchema.validate(obj, schema),
{ valid: true, errors: [] },
"Substitution of a valid variable validates"
);
}
{
const obj = "https://mozilla.org/%BOGUS%/";
Assert.equal(
Services.urlFormatter.formatURL(obj),
obj,
"BOGUS is an invalid variable for the URL formatter service"
);
Assert.ok(
!JsonSchema.validate(obj, { type: "string", format: "uri" }).valid,
"A moz-url-format string does not validate as a URI"
);
Assert.deepEqual(
JsonSchema.validate(obj, schema),
{
valid: false,
errors: [
{
instanceLocation: "#",
keyword: "format",
keywordLocation: "#/format",
error: `String does not match format "moz-url-format".`,
},
],
},
"Substitution of an invalid variable does not validate"
);
}
});

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

@ -36,7 +36,6 @@ tags = remote-settings
[test_Integration.js]
[test_jsesc.js]
[test_JSONFile.js]
[test_JsonSchema.js]
[test_Log.js]
[test_MatchURLFilters.js]
[test_NewTabUtils.js]