Bug 1716736 - Add schema validation for experiment enrollments in tests r=k88hudson

Differential Revision: https://phabricator.services.mozilla.com/D118367
This commit is contained in:
Andrei Oprea 2021-07-26 14:48:22 +00:00
Родитель 2249630d55
Коммит a66714326b
26 изменённых файлов: 493 добавлений и 178 удалений

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

@ -111,8 +111,8 @@ add_task(async function test_remote_configuration() {
feature: NimbusFeatures.aboutwelcome,
configuration: {
slug: "about:studies-configuration-slug",
enabled: true,
variables: {},
variables: { enabled: true },
targeting: "true",
},
});

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

@ -26,7 +26,11 @@ add_task(async function remote_disable() {
await ExperimentFakes.remoteDefaultsHelper({
feature: NimbusFeatures.shellService,
configuration: { variables: { disablePin: true } },
configuration: {
slug: "shellService_remoteDisable",
variables: { disablePin: true, enabled: true },
targeting: "true",
},
});
Assert.equal(
@ -42,10 +46,7 @@ add_task(async function restore_default() {
return;
}
await ExperimentFakes.remoteDefaultsHelper({
feature: NimbusFeatures.shellService,
configuration: {},
});
ExperimentAPI._store._deleteForTests("shellService");
Assert.equal(
await ShellService.doesAppNeedPin(),

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

@ -48,7 +48,14 @@ add_task(async function remote_disable() {
setDefaultStub.resetHistory();
await ExperimentFakes.remoteDefaultsHelper({
feature: NimbusFeatures.shellService,
configuration: { variables: { setDefaultBrowserUserChoice: false } },
configuration: {
slug: "shellService_remoteDisable",
variables: {
setDefaultBrowserUserChoice: false,
enabled: true,
},
targeting: "true",
},
});
ShellService.setDefaultBrowser();
@ -68,10 +75,7 @@ add_task(async function restore_default() {
userChoiceStub.resetHistory();
setDefaultStub.resetHistory();
await ExperimentFakes.remoteDefaultsHelper({
feature: NimbusFeatures.shellService,
configuration: {},
});
ExperimentAPI._store._deleteForTests("shellService");
ShellService.setDefaultBrowser();

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

@ -360,7 +360,11 @@ add_task(async function remote_disabled() {
await ExperimentAPI.ready();
await ExperimentFakes.remoteDefaultsHelper({
feature: NimbusFeatures.upgradeDialog,
configuration: { enabled: false, variables: {} },
configuration: {
slug: "upgradeDialog_remoteDisabled",
variables: { enabled: false },
targeting: "true",
},
});
// Simulate starting from a previous version.
@ -380,7 +384,11 @@ add_task(async function remote_disabled() {
// Re-enable back
await ExperimentFakes.remoteDefaultsHelper({
feature: NimbusFeatures.upgradeDialog,
configuration: { enabled: true, variables: {} },
configuration: {
slug: "upgradeDialog_remoteEnabled",
variables: { enabled: true },
targeting: "true",
},
});
});

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

@ -183,9 +183,7 @@ const ExperimentAPI = {
}
let fullEventName = `${eventName}:${options.slug || options.featureId}`;
// The update event will always fire after the event listener is added, either
// immediately if it is already ready, or on ready
this._store.ready().then(() => {
if (this._store._isReady) {
let experiment = this.getExperiment(options);
// Only if we have an experiment that matches what the caller requested
if (experiment) {
@ -194,7 +192,7 @@ const ExperimentAPI = {
// are attached later than the `update` events.
callback(fullEventName, experiment);
}
});
}
this._store.on(fullEventName, callback);
},

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

@ -30,6 +30,7 @@ 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",
]

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

@ -79,6 +79,74 @@
},
"required": ["id", "configurations"],
"additionalProperties": false
},
"RemoteFeatureConfiguration": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Configuration identifier that will be included in Telemetry."
},
"isEarlyStartup": {
"type": "boolean",
"description": "If the feature values should be cached in prefs for fast early startup."
},
"variables": {
"type": "object",
"description": "Key value pairs that should match the feature manifest definition.",
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": ["enabled"]
},
"targeting": {
"type": "string",
"description": "Target the configuration only to specific clients."
},
"bucketConfig": {
"type": "object",
"properties": {
"randomizationUnit": {
"type": "string",
"description": "A unique, stable identifier for the user used as an input to bucket hashing"
},
"namespace": {
"type": "string",
"description": "Additional inputs to the hashing function"
},
"start": {
"type": "number",
"description": "Index of start of the range of buckets"
},
"count": {
"type": "number",
"description": "Number of buckets to check"
},
"total": {
"type": "number",
"description": "Total number of buckets",
"default": 10000
}
},
"required": [
"randomizationUnit",
"namespace",
"start",
"count",
"total"
],
"additionalProperties": false,
"description": "Bucketing configuration"
},
"description": {
"type": "string",
"description": "Explanation for configuration and targeting"
}
},
"required": ["variables", "targeting", "slug"],
"additionalProperties": false
}
}
}

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

@ -0,0 +1,91 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/NimbusExperiment",
"definitions": {
"NimbusExperiment": {
"type": "object",
"properties": {
"slug": {
"type": "string"
},
"branch": {
"type": "object",
"properties": {
"feature": {
"type": "object",
"properties": {
"featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"value": {
"anyOf": [
{
"type": "object",
"additionalProperties": {}
},
{
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
},
"enabled": {
"type": "boolean",
"description": "(deprecated)"
}
},
"required": ["featureId", "value"],
"additionalProperties": false
}
},
"required": ["feature"]
},
"active": {
"type": "boolean",
"description": "Experiment status"
},
"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",
"description": "When was the enrollment made"
},
"force": {
"type": "boolean",
"description": "(debug) If the enrollment happened naturally or through devtools"
}
},
"required": [
"slug",
"branch",
"active",
"enrollmentId",
"experimentType",
"source",
"userFacingName",
"userFacingDescription"
],
"additionalProperties": false,
"description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"
}
}
}

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

@ -20,47 +20,94 @@ XPCOMUtils.defineLazyModuleGetters(this, {
_RemoteSettingsExperimentLoader:
"resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm",
Ajv: "resource://testing-common/ajv-4.1.1.js",
sinon: "resource://testing-common/Sinon.jsm",
});
const { SYNC_DATA_PREF_BRANCH, SYNC_DEFAULTS_PREF_BRANCH } = ExperimentStore;
const PATH = FileTestUtils.getTempFile("shared-data-map").path;
XPCOMUtils.defineLazyGetter(this, "fetchExperimentSchema", async () => {
const response = await fetch(
"resource://testing-common/NimbusExperiment.schema.json"
);
async function fetchSchema(url) {
const response = await fetch(url);
const schema = await response.json();
if (!schema) {
throw new Error("Failed to load NimbusSchema");
throw new Error(`Failed to load ${url}`);
}
return schema.definitions.NimbusExperiment;
});
return schema.definitions;
}
const EXPORTED_SYMBOLS = ["ExperimentTestUtils", "ExperimentFakes"];
const ExperimentTestUtils = {
/**
* Checks if an experiment is valid acording to existing schema
* @param {NimbusExperiment} experiment
*/
async validateExperiment(experiment) {
const schema = await fetchExperimentSchema;
_validator(schema, value, errorMsg) {
const ajv = new Ajv({ async: "co*", allErrors: true });
const validator = ajv.compile(schema);
validator(experiment);
validator(value);
if (validator.errors?.length) {
throw new Error(
"Experiment not valid:" + JSON.stringify(validator.errors, undefined, 2)
`${errorMsg}: ${JSON.stringify(validator.errors, undefined, 2)}`
);
}
return experiment;
return value;
},
/**
* Checks if an experiment is valid acording to existing schema
*/
async validateExperiment(experiment) {
const schema = (
await fetchSchema(
"resource://testing-common/NimbusExperiment.schema.json"
)
).NimbusExperiment;
return this._validator(
schema,
experiment,
`Experiment ${experiment.slug} not valid`
);
},
async validateEnrollment(enrollment) {
const schema = (
await fetchSchema(
"resource://testing-common/NimbusEnrollment.schema.json"
)
).NimbusExperiment;
return this._validator(
schema,
enrollment,
`Enrollment ${enrollment.slug} is not valid`
);
},
async validateRollouts(rollout) {
const schema = (
await fetchSchema(
"resource://testing-common/ExperimentFeatureRemote.schema.json"
)
).RemoteFeatureConfiguration;
return this._validator(
schema,
rollout,
`Rollout configuration ${rollout.slug} is not valid`
);
},
};
const ExperimentFakes = {
manager(store) {
return new _ExperimentManager({ store: store || this.store() });
let sandbox = sinon.createSandbox();
let manager = new _ExperimentManager({ store: store || this.store() });
// We want calls to `store.addExperiment` to implicitly validate the
// enrollment before saving to store
let origAddExperiment = manager.store.addExperiment.bind(manager.store);
sandbox.stub(manager.store, "addExperiment").callsFake(async enrollment => {
await ExperimentTestUtils.validateEnrollment(enrollment);
return origAddExperiment(enrollment);
});
return manager;
},
store() {
return new ExperimentStore("FakeStore", { path: PATH, isParent: true });
@ -72,7 +119,7 @@ const ExperimentFakes = {
return new Promise(resolve => ExperimentAPI.on("update", options, resolve));
},
remoteDefaultsHelper({
async remoteDefaultsHelper({
feature,
store = ExperimentManager.store,
configuration,
@ -80,9 +127,14 @@ const ExperimentFakes = {
if (!store._isReady) {
throw new Error("Store not ready, need to `await ExperimentAPI.ready()`");
}
store.updateRemoteConfigs(feature.featureId, configuration);
return feature.ready().then(() => store._syncToChildren({ flush: true }));
await ExperimentTestUtils.validateRollouts(configuration);
// After storing the remote configuration to store and updating the feature
// we want to flush so that NimbusFeature usage in content process also
// receives the update
store.updateRemoteConfigs(feature.featureId, configuration);
await feature.ready();
store._syncToChildren({ flush: true });
},
async enrollWithFeatureConfig(
featureConfig,
@ -152,7 +204,7 @@ const ExperimentFakes = {
if (!manager.store._isReady) {
throw new Error("Manager store not ready, call `manager.onStartup`");
}
manager.enroll(recipe);
manager.enroll(recipe, "enrollmentHelper");
}
return { enrollmentPromise, doExperimentCleanup };
@ -193,8 +245,11 @@ const ExperimentFakes = {
},
...props,
},
source: "test",
source: "NimbusTestUtils",
isEnrollmentPaused: true,
experimentType: "NimbusTestUtils",
userFacingName: "NimbusTestUtils",
userFacingDescription: "NimbusTestUtils",
...props,
};
},

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

@ -75,10 +75,26 @@ add_task(async function test_evaluate_active_experiments_activeExperiments() {
const slug = "foo" + Math.random();
// Init the store before we use it
await ExperimentManager.onStartup();
ExperimentManager.store.addExperiment(ExperimentFakes.experiment(slug));
registerCleanupFunction(() => {
ExperimentManager.store._deleteForTests(slug);
});
let {
enrollmentPromise,
doExperimentCleanup,
} = ExperimentFakes.enrollmentHelper(
ExperimentFakes.recipe(slug, {
branches: [
{
slug: "mochitest-active-foo",
feature: {
enabled: true,
featureId: "foo",
value: null,
},
},
],
active: true,
})
);
await enrollmentPromise;
Assert.equal(
await RemoteSettingsExperimentLoader.evaluateJexl(
@ -97,4 +113,6 @@ add_task(async function test_evaluate_active_experiments_activeExperiments() {
false,
"should not find an experiment that doesn't exist"
);
await doExperimentCleanup();
});

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

@ -48,14 +48,10 @@ add_task(async function test_double_feature_enrollment() {
let enrollPromise1 = ExperimentFakes.waitForExperimentUpdate(ExperimentAPI, {
slug: recipe1.slug,
});
let enrollPromise2 = ExperimentFakes.waitForExperimentUpdate(ExperimentAPI, {
slug: recipe2.slug,
});
ExperimentManager.enroll(recipe1);
ExperimentManager.enroll(recipe2);
await Promise.any([enrollPromise1, enrollPromise2]);
ExperimentManager.enroll(recipe1, "test_double_feature_enrollment");
await enrollPromise1;
ExperimentManager.enroll(recipe2, "test_double_feature_enrollment");
Assert.equal(
ExperimentManager.store.getAllActive().length,

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

@ -23,9 +23,6 @@ const {
const { BrowserTestUtils } = ChromeUtils.import(
"resource://testing-common/BrowserTestUtils.jsm"
);
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { TelemetryEnvironment } = ChromeUtils.import(
"resource://gre/modules/TelemetryEnvironment.jsm"
);
@ -36,8 +33,7 @@ const REMOTE_CONFIGURATION_AW = {
configurations: [
{
slug: "a",
variables: { remoteValue: 24 },
enabled: false,
variables: { remoteValue: 24, enabled: false },
targeting: "false",
bucketConfig: {
namespace: "nimbus-test-utils",
@ -49,8 +45,7 @@ const REMOTE_CONFIGURATION_AW = {
},
{
slug: "b",
variables: { remoteValue: 42 },
enabled: true,
variables: { remoteValue: 42, enabled: true },
targeting: "true",
bucketConfig: {
namespace: "nimbus-test-utils",
@ -68,8 +63,7 @@ const REMOTE_CONFIGURATION_NEWTAB = {
configurations: [
{
slug: "a",
variables: { remoteValue: 1 },
enabled: false,
variables: { remoteValue: 1, enabled: false },
targeting: "false",
bucketConfig: {
namespace: "nimbus-test-utils",
@ -81,8 +75,7 @@ const REMOTE_CONFIGURATION_NEWTAB = {
},
{
slug: "b",
variables: { remoteValue: 3 },
enabled: true,
variables: { remoteValue: 3, enabled: true },
targeting: "true",
bucketConfig: {
namespace: "nimbus-test-utils",
@ -94,8 +87,7 @@ const REMOTE_CONFIGURATION_NEWTAB = {
},
{
slug: "c",
variables: { remoteValue: 2 },
enabled: false,
variables: { remoteValue: 2, enabled: false },
targeting: "false",
bucketConfig: {
namespace: "nimbus-test-utils",
@ -487,7 +479,8 @@ add_task(async function remote_defaults_no_mutation() {
add_task(async function remote_defaults_active_experiments_check() {
let barFeature = new ExperimentFeature("bar", {
bar: { description: "mochitest" },
description: "mochitest",
variables: { enabled: { type: "boolean" } },
});
let experimentOnlyRemoteDefault = {
id: "bar",
@ -495,14 +488,12 @@ add_task(async function remote_defaults_active_experiments_check() {
configurations: [
{
slug: "a",
variables: {},
enabled: false,
variables: { enabled: false },
targeting: "'mochitest-active-foo' in activeExperiments",
},
{
slug: "b",
variables: {},
enabled: true,
variables: { enabled: true },
targeting: "true",
},
],
@ -550,10 +541,12 @@ add_task(async function remote_defaults_active_remote_defaults() {
ExperimentAPI._store._deleteForTests("foo");
ExperimentAPI._store._deleteForTests("bar");
let barFeature = new ExperimentFeature("bar", {
bar: { description: "mochitest" },
description: "mochitest",
variables: { enabled: { type: "boolean" } },
});
let fooFeature = new ExperimentFeature("foo", {
foo: { description: "mochitest" },
description: "mochitest",
variables: { enabled: { type: "boolean" } },
});
let remoteDefaults = [
{
@ -562,8 +555,7 @@ add_task(async function remote_defaults_active_remote_defaults() {
configurations: [
{
slug: "a",
variables: {},
enabled: true,
variables: { enabled: true },
targeting: "true",
},
],
@ -574,8 +566,7 @@ add_task(async function remote_defaults_active_remote_defaults() {
configurations: [
{
slug: "b",
variables: {},
enabled: true,
variables: { enabled: true },
targeting: "'bar' in activeRemoteDefaults",
},
],
@ -647,14 +638,12 @@ add_task(async function test_remote_defaults_no_bucketConfig() {
configurations: [
{
slug: "a",
variables: { remoteValue: 24 },
enabled: false,
variables: { remoteValue: 24, enabled: false },
targeting: "false",
},
{
slug: "b",
variables: { remoteValue: 42 },
enabled: true,
variables: { remoteValue: 42, enabled: true },
targeting: "true",
},
],
@ -688,7 +677,23 @@ add_task(async function test_remote_defaults_no_bucketConfig() {
add_task(async function remote_defaults_variables_storage() {
let barFeature = new ExperimentFeature("bar", {
bar: { description: "mochitest" },
bar: {
description: "mochitest",
variables: {
storage: {
type: "int",
},
object: {
type: "json",
},
string: {
type: "string",
},
bool: {
type: "boolean",
},
},
},
});
let remoteDefaults = [
{
@ -703,8 +708,8 @@ add_task(async function remote_defaults_variables_storage() {
object: { foo: "foo" },
string: "string",
bool: true,
enabled: true,
},
enabled: true,
targeting: "true",
},
],
@ -715,6 +720,10 @@ add_task(async function remote_defaults_variables_storage() {
await RemoteDefaultsLoader.syncRemoteDefaults("mochitest");
await barFeature.ready();
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}bar`, ""),
"Experiment stored in prefs"
);
Assert.ok(
Services.prefs.getIntPref(`${SYNC_DEFAULTS_PREF_BRANCH}bar.storage`, 0),
"Stores variable in separate pref"

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

@ -5,3 +5,55 @@
// Globals
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
Ajv: "resource://testing-common/ajv-4.1.1.js",
ExperimentTestUtils: "resource://testing-common/NimbusTestUtils.jsm",
RemoteDefaultsLoader:
"resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm",
ExperimentFakes: "resource://testing-common/NimbusTestUtils.jsm",
});
add_task(function setup() {
let sandbox = sinon.createSandbox();
/* We stub the functions that operate with enrollments and remote rollouts
* so that any access to store something is implicitly validated against
* the schema and no records have missing (or extra) properties while in tests
*/
let origAddExperiment = ExperimentManager.store.addExperiment.bind(
ExperimentManager.store
);
let origOnUpdatesReady = RemoteDefaultsLoader._onUpdatesReady.bind(
RemoteDefaultsLoader
);
sandbox
.stub(ExperimentManager.store, "addExperiment")
.callsFake(async enrollment => {
await ExperimentTestUtils.validateEnrollment(enrollment);
return origAddExperiment(enrollment);
});
// Unlike `addExperiment` the method to store remote rollouts is syncronous
// and our validation method would turn it async. If we had changed to `await`
// for remote configs storage it would have changed the code logic so we are
// going up one level to the function that receives the RS records and do
// the validation there.
sandbox
.stub(RemoteDefaultsLoader, "_onUpdatesReady")
.callsFake(async (remoteDefaults, reason) => {
for (let remoteDefault of remoteDefaults) {
for (let config of remoteDefault.configurations) {
await ExperimentTestUtils.validateRollouts(config);
}
}
return origOnUpdatesReady(remoteDefaults, reason);
});
registerCleanupFunction(() => {
sandbox.restore();
});
});

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

@ -3,3 +3,9 @@
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
ExperimentFakes: "resource://testing-common/NimbusTestUtils.jsm",
});

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

@ -27,7 +27,7 @@ add_task(async function test_getExperiment_fromChild_slug() {
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
// Wait to sync to child
await TestUtils.waitForCondition(
@ -59,7 +59,7 @@ add_task(async function test_getExperiment_fromParent_slug() {
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
await ExperimentAPI.ready();
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
Assert.equal(
ExperimentAPI.getExperiment({ slug: "foo" }).slug,
@ -80,7 +80,7 @@ add_task(async function test_getExperimentMetaData() {
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
await ExperimentAPI.ready();
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
let metadata = ExperimentAPI.getExperimentMetaData({ slug: expected.slug });
@ -140,7 +140,7 @@ add_task(async function test_getExperiment_feature() {
branch: {
slug: "treatment",
value: { title: "hi" },
feature: { featureId: "cfr", enabled: true },
feature: { featureId: "cfr", enabled: true, value: null },
},
});
@ -149,7 +149,7 @@ add_task(async function test_getExperiment_feature() {
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
// Wait to sync to child
await TestUtils.waitForCondition(
@ -268,7 +268,7 @@ add_task(async function test_addExperiment_eventEmit_add() {
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "purple", enabled: true },
feature: { featureId: "purple", enabled: true, value: null },
},
});
const store = ExperimentFakes.store();
@ -280,7 +280,7 @@ add_task(async function test_addExperiment_eventEmit_add() {
ExperimentAPI.on("update", { slug: "foo" }, slugStub);
ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
store.addExperiment(experiment);
await store.addExperiment(experiment);
Assert.equal(
slugStub.callCount,
@ -303,7 +303,7 @@ add_task(async function test_updateExperiment_eventEmit_add_and_update() {
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "purple", enabled: true },
feature: { featureId: "purple", enabled: true, value: null },
},
});
const store = ExperimentFakes.store();
@ -312,7 +312,7 @@ add_task(async function test_updateExperiment_eventEmit_add_and_update() {
await store.init();
await ExperimentAPI.ready();
store.addExperiment(experiment);
await store.addExperiment(experiment);
ExperimentAPI.on("update", { slug: "foo" }, slugStub);
ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
@ -337,7 +337,7 @@ add_task(async function test_updateExperiment_eventEmit_off() {
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "purple", enabled: true },
feature: { featureId: "purple", enabled: true, value: null },
},
});
const store = ExperimentFakes.store();
@ -349,7 +349,7 @@ add_task(async function test_updateExperiment_eventEmit_off() {
ExperimentAPI.on("update", { slug: "foo" }, slugStub);
ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
store.addExperiment(experiment);
await store.addExperiment(experiment);
ExperimentAPI.off("update:foo", slugStub);
ExperimentAPI.off("update:purple", featureStub);
@ -367,12 +367,12 @@ add_task(async function test_activateBranch() {
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "green", enabled: true },
feature: { featureId: "green", enabled: true, value: null },
},
});
await store.init();
store.addExperiment(experiment);
await store.addExperiment(experiment);
Assert.deepEqual(
ExperimentAPI.activateBranch({ featureId: "green" }),
@ -412,7 +412,7 @@ add_task(async function test_activateBranch_activationEvent() {
});
await store.init();
store.addExperiment(experiment);
await store.addExperiment(experiment);
// Adding stub later because `addExperiment` emits update events
const stub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
ExperimentAPI.activateBranch({ featureId: "green" });
@ -449,7 +449,7 @@ add_task(async function test_activateBranch_storeFailure() {
});
await store.init();
store.addExperiment(experiment);
await store.addExperiment(experiment);
// Adding stub later because `addExperiment` emits update events
const stub = sandbox.stub(store, "emit");
// Call activateBranch to trigger an activation event
@ -476,7 +476,7 @@ add_task(async function test_activateBranch_noActivationEvent() {
});
await store.init();
store.addExperiment(experiment);
await store.addExperiment(experiment);
// Adding stub later because `addExperiment` emits update events
const stub = sandbox.stub(store, "emit");
// Call activateBranch to trigger an activation event

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

@ -40,6 +40,7 @@ const FAKE_FEATURE_MANIFEST = {
},
};
const FAKE_FEATURE_REMOTE_VALUE = {
slug: "default-remote-value",
variables: {
enabled: true,
},
@ -68,7 +69,7 @@ add_task(async function test_ExperimentFeature_ready() {
},
});
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
await readyPromise;
@ -121,7 +122,7 @@ add_task(
},
});
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
setDefaultBranch(TEST_FALLBACK_PREF, `{"bar": 123}`);
@ -226,7 +227,10 @@ add_task(async function test_ExperimentFeature_test_helper_ready() {
await ExperimentFakes.remoteDefaultsHelper({
feature: featureInstance,
store: manager.store,
configuration: { variables: { remoteValue: "mochitest" }, enabled: true },
configuration: {
...FAKE_FEATURE_REMOTE_VALUE,
variables: { remoteValue: "mochitest", enabled: true },
},
});
Assert.equal(featureInstance.isEnabled(), true, "enabled by remote config");
@ -253,7 +257,7 @@ add_task(
await manager.store.ready();
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
manager.store.updateRemoteConfigs("foo", {
@ -305,7 +309,7 @@ add_task(async function test_ExperimentFeature_isEnabled_no_exposure() {
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
manager.store.addExperiment(expected);
await manager.store.addExperiment(expected);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
@ -334,7 +338,7 @@ add_task(async function test_record_exposure_event() {
"should not emit an exposure event when no experiment is active"
);
manager.store.addExperiment(
await manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",
@ -363,7 +367,7 @@ add_task(async function test_record_exposure_event_once() {
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
manager.store.addExperiment(
await manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",
@ -391,7 +395,7 @@ add_task(async function test_prevent_double_exposure_getValue() {
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
manager.store.addExperiment(
await manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",
@ -422,7 +426,7 @@ add_task(async function test_prevent_double_exposure_isEnabled() {
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
manager.store.addExperiment(
await manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",
@ -452,13 +456,15 @@ add_task(async function test_set_remote_before_ready() {
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
const feature = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
Assert.throws(
() =>
ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: { variables: { test: true } },
}),
await Assert.rejects(
ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: {
...FAKE_FEATURE_REMOTE_VALUE,
variables: { test: true, enabled: true },
},
}),
/Store not ready/,
"Throws if used before init finishes"
);
@ -468,7 +474,10 @@ add_task(async function test_set_remote_before_ready() {
await ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: { variables: { test: true } },
configuration: {
...FAKE_FEATURE_REMOTE_VALUE,
variables: { test: true, enabled: true },
},
});
Assert.ok(feature.getValue().test, "Successfully set");
@ -494,12 +503,15 @@ add_task(async function test_isEnabled_backwards_compatible() {
await ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: { variables: {}, enabled: false },
configuration: {
...FAKE_FEATURE_REMOTE_VALUE,
variables: { enabled: false },
},
});
Assert.ok(!feature.isEnabled(), "Disabled based on remote configs");
manager.store.addExperiment(
await manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",

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

@ -11,10 +11,6 @@ const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { cleanupStorePrefCache } = ExperimentFakes;
async function setupForExperimentFeature() {
@ -88,7 +84,7 @@ add_task(
},
});
manager.store.addExperiment(recipe);
await manager.store.addExperiment(recipe);
const featureInstance = new ExperimentFeature(
FEATURE_ID,

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

@ -11,10 +11,6 @@ const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { cleanupStorePrefCache } = ExperimentFakes;
async function setupForExperimentFeature() {
@ -82,7 +78,7 @@ add_task(async function test_ExperimentFeature_getValue_prefsOverExperiment() {
},
});
manager.store.addExperiment(recipe);
await manager.store.addExperiment(recipe);
const featureInstance = new ExperimentFeature(
FEATURE_ID,

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

@ -4,15 +4,9 @@ const {
ExperimentAPI,
_ExperimentFeature: ExperimentFeature,
} = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);

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

@ -5,17 +5,11 @@ const {
NimbusFeatures,
_ExperimentFeature: ExperimentFeature,
} = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
const { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
Cu.importGlobalProperties(["fetch"]);
XPCOMUtils.defineLazyGetter(this, "fetchSchema", async () => {

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

@ -1,8 +1,5 @@
"use strict";
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { NormandyTestUtils } = ChromeUtils.import(
"resource://testing-common/NormandyTestUtils.jsm"
);
@ -31,10 +28,14 @@ const { SYNC_DATA_PREF_BRANCH } = ExperimentStore;
add_task(async function test_add_to_store() {
const manager = ExperimentFakes.manager();
const recipe = ExperimentFakes.recipe("foo");
const enrollPromise = new Promise(resolve =>
manager.store.on("update:foo", resolve)
);
await manager.onStartup();
await manager.enroll(recipe);
await manager.enroll(recipe, "test_add_to_store");
await enrollPromise;
const experiment = manager.store.get("foo");
Assert.ok(experiment, "should add an experiment with slug foo");
@ -53,6 +54,9 @@ add_task(
async function test_setExperimentActive_sendEnrollmentTelemetry_called() {
const manager = ExperimentFakes.manager();
const sandbox = sinon.createSandbox();
const enrollPromise = new Promise(resolve =>
manager.store.on("update:foo", resolve)
);
sandbox.spy(manager, "setExperimentActive");
sandbox.spy(manager, "sendEnrollmentTelemetry");
@ -60,7 +64,11 @@ add_task(
await manager.onStartup();
await manager.enroll(ExperimentFakes.recipe("foo"));
await manager.enroll(
ExperimentFakes.recipe("foo"),
"test_setExperimentActive_sendEnrollmentTelemetry_called"
);
await enrollPromise;
const experiment = manager.store.get("foo");
Assert.equal(
@ -91,10 +99,10 @@ add_task(async function test_failure_name_conflict() {
await manager.onStartup();
// simulate adding a previouly enrolled experiment
manager.store.addExperiment(ExperimentFakes.experiment("foo"));
await manager.store.addExperiment(ExperimentFakes.experiment("foo"));
await Assert.rejects(
manager.enroll(ExperimentFakes.recipe("foo")),
manager.enroll(ExperimentFakes.recipe("foo"), "test_failure_name_conflict"),
/An experiment with the slug "foo" already exists/,
"should throw if a conflicting experiment exists"
);
@ -121,15 +129,15 @@ add_task(async function test_failure_group_conflict() {
// These should not be allowed to exist simultaneously.
const existingBranch = {
slug: "treatment",
feature: { featureId: "pink", enabled: true },
feature: { featureId: "pink", enabled: true, value: {} },
};
const newBranch = {
slug: "treatment",
feature: { featureId: "pink", enabled: true },
feature: { featureId: "pink", enabled: true, value: {} },
};
// simulate adding an experiment with a conflicting group "pink"
manager.store.addExperiment(
await manager.store.addExperiment(
ExperimentFakes.experiment("foo", {
branch: existingBranch,
})
@ -139,7 +147,8 @@ add_task(async function test_failure_group_conflict() {
sandbox.stub(manager, "chooseBranch").returns(newBranch);
Assert.equal(
await manager.enroll(
ExperimentFakes.recipe("bar", { branches: [newBranch] })
ExperimentFakes.recipe("bar", { branches: [newBranch] }),
"test_failure_group_conflict"
),
null,
"should not enroll if there is a feature conflict"
@ -232,8 +241,12 @@ add_task(async function enroll_in_reference_aw_experiment() {
recipe.bucketConfig.count = recipe.bucketConfig.total;
const manager = ExperimentFakes.manager();
const enrollPromise = new Promise(resolve =>
manager.store.on("update:reference-aw", resolve)
);
await manager.onStartup();
await manager.enroll(recipe);
await manager.enroll(recipe, "enroll_in_reference_aw_experiment");
await enrollPromise;
Assert.ok(manager.store.get("reference-aw"), "Successful onboarding");
let prefValue = Services.prefs.getStringPref(
@ -251,13 +264,19 @@ add_task(async function enroll_in_reference_aw_experiment() {
add_task(async function test_forceEnroll_cleanup() {
const manager = ExperimentFakes.manager();
const sandbox = sinon.createSandbox();
const fooEnrollPromise = new Promise(resolve =>
manager.store.on("update:foo", resolve)
);
const barEnrollPromise = new Promise(resolve =>
manager.store.on("update:optin-bar", resolve)
);
let unenrollStub = sandbox.spy(manager, "unenroll");
let existingRecipe = ExperimentFakes.recipe("foo", {
branches: [
{
slug: "treatment",
ratio: 1,
feature: { featureId: "force-enrollment", enabled: true },
feature: { featureId: "force-enrollment", enabled: true, value: {} },
},
],
});
@ -266,16 +285,18 @@ add_task(async function test_forceEnroll_cleanup() {
{
slug: "treatment",
ratio: 1,
feature: { featureId: "force-enrollment", enabled: true },
feature: { featureId: "force-enrollment", enabled: true, value: {} },
},
],
});
await manager.onStartup();
await manager.enroll(existingRecipe);
await manager.enroll(existingRecipe, "test_forceEnroll_cleanup");
await fooEnrollPromise;
let setExperimentActiveSpy = sandbox.spy(manager, "setExperimentActive");
manager.forceEnroll(forcedRecipe, forcedRecipe.branches[0]);
await barEnrollPromise;
Assert.ok(unenrollStub.called, "Unenrolled from existing experiment");
Assert.equal(

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

@ -6,9 +6,6 @@ const { _ExperimentManager } = ChromeUtils.import(
const { ExperimentStore } = ChromeUtils.import(
"resource://nimbus/lib/ExperimentStore.jsm"
);
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { Sampling } = ChromeUtils.import(
"resource://gre/modules/components-utils/Sampling.jsm"
);
@ -163,8 +160,13 @@ add_task(async function test_onRecipe_isEnrollmentPaused() {
const updatedRecipe = ExperimentFakes.recipe("foo", {
isEnrollmentPaused: true,
});
await manager.enroll(fooRecipe);
let enrollmentPromise = new Promise(resolve =>
manager.store.on(`update:${fooRecipe.slug}`, resolve)
);
await manager.enroll(fooRecipe, "test");
await enrollmentPromise;
await manager.onRecipe(updatedRecipe, "test");
console.log("XXX", manager.updateEnrollment.callCount);
Assert.equal(
manager.updateEnrollment.calledWith(updatedRecipe),
true,
@ -186,10 +188,15 @@ add_task(async function test_onFinalize_unenroll() {
// Add an experiment to the store without calling .onRecipe
// This simulates an enrollment having happened in the past.
manager.store.addExperiment(ExperimentFakes.experiment("foo"));
let recipe0 = ExperimentFakes.experiment("foo", {
experimentType: "unittest",
userFacingName: "foo",
userFacingDescription: "foo",
lastSeen: Date.now().toLocaleString(),
source: "test",
});
await manager.store.addExperiment(recipe0);
// Simulate adding some other recipes
await manager.onStartup();
const recipe1 = ExperimentFakes.recipe("bar");
// Unique features to prevent overlap
recipe1.branches[0].feature.featureId = "red";

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

@ -1,8 +1,5 @@
"use strict";
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { NormandyTestUtils } = ChromeUtils.import(
"resource://testing-common/NormandyTestUtils.jsm"
);
@ -31,7 +28,7 @@ add_task(async function test_set_inactive() {
const manager = ExperimentFakes.manager();
await manager.onStartup();
manager.store.addExperiment(ExperimentFakes.experiment("foo"));
await manager.store.addExperiment(ExperimentFakes.experiment("foo"));
manager.unenroll("foo", "some-reason");
@ -49,7 +46,7 @@ add_task(async function test_unenroll_opt_out() {
const experiment = ExperimentFakes.experiment("foo");
await manager.onStartup();
manager.store.addExperiment(experiment);
await manager.store.addExperiment(experiment);
Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, false);
@ -83,7 +80,7 @@ add_task(async function test_setExperimentInactive_called() {
const experiment = ExperimentFakes.experiment("foo");
await manager.onStartup();
manager.store.addExperiment(experiment);
await manager.store.addExperiment(experiment);
manager.unenroll("foo", "some-reason");
@ -99,7 +96,7 @@ add_task(async function test_send_unenroll_event() {
const experiment = ExperimentFakes.experiment("foo");
await manager.onStartup();
manager.store.addExperiment(experiment);
await manager.store.addExperiment(experiment);
manager.unenroll("foo", "some-reason");
@ -126,7 +123,7 @@ add_task(async function test_undefined_reason() {
const experiment = ExperimentFakes.experiment("foo");
await manager.onStartup();
manager.store.addExperiment(experiment);
await manager.store.addExperiment(experiment);
manager.unenroll("foo");

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

@ -1,8 +1,5 @@
"use strict";
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { ExperimentStore } = ChromeUtils.import(
"resource://nimbus/lib/ExperimentStore.jsm"
);

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

@ -4,9 +4,6 @@ const { FeatureManifest } = ChromeUtils.import(
"resource://nimbus/FeatureManifest.js"
);
const { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
Cu.importGlobalProperties(["fetch"]);
XPCOMUtils.defineLazyGetter(this, "fetchSchema", async () => {

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

@ -7,9 +7,6 @@ const { FileTestUtils } = ChromeUtils.import(
const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const PATH = FileTestUtils.getTempFile("shared-data-map").path;