Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
const { ExperimentStore } = ChromeUtils.importESModule(
);
const { FeatureManifest } = ChromeUtils.importESModule(
);
const { SYNC_DATA_PREF_BRANCH, SYNC_DEFAULTS_PREF_BRANCH } = ExperimentStore;
const { cleanupStorePrefCache } = ExperimentFakes;
add_task(async function test_sharedDataMap_key() {
const store = new ExperimentStore();
// Outside of tests we use sharedDataKey for the profile dir filepath
// where we store experiments
Assert.ok(store._sharedDataKey, "Make sure it's defined");
});
add_task(async function test_usageBeforeInitialization() {
const store = ExperimentFakes.store();
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [{ featureId: "purple" }],
},
});
Assert.equal(store.getAll().length, 0, "It should not fail");
await store.init();
store.addEnrollment(experiment);
Assert.equal(
store.getExperimentForFeature("purple"),
experiment,
"should return a matching experiment for the given feature"
);
});
add_task(async function test_event_add_experiment() {
const sandbox = sinon.createSandbox();
const store = ExperimentFakes.store();
const expected = ExperimentFakes.experiment("foo");
const updateEventCbStub = sandbox.stub();
// Setup ExperimentManager and child store for ExperimentAPI
await store.init();
// Set update cb
store.on("update:foo", updateEventCbStub);
// Add some data
store.addEnrollment(expected);
Assert.equal(updateEventCbStub.callCount, 1, "Called once for add");
store.off("update:foo", updateEventCbStub);
});
add_task(async function test_event_updates_main() {
const sandbox = sinon.createSandbox();
const store = ExperimentFakes.store();
const experiment = ExperimentFakes.experiment("foo");
const updateEventCbStub = sandbox.stub();
// Setup ExperimentManager and child store for ExperimentAPI
await store.init();
// Set update cb
store.on(
`featureUpdate:${experiment.branch.features[0].featureId}`,
updateEventCbStub
);
store.addEnrollment(experiment);
store.updateExperiment("foo", { active: false });
Assert.equal(
updateEventCbStub.callCount,
2,
"Should be called twice: add, update"
);
Assert.equal(
updateEventCbStub.firstCall.args[1],
"experiment-updated",
"Should be called with updated experiment status"
);
Assert.equal(
updateEventCbStub.secondCall.args[1],
"experiment-updated",
"Should be called with updated experiment status"
);
store.off(
`featureUpdate:${experiment.branch.features[0].featureId}`,
updateEventCbStub
);
});
add_task(async function test_getExperimentForGroup() {
const store = ExperimentFakes.store();
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [{ featureId: "purple" }],
},
});
await store.init();
store.addEnrollment(ExperimentFakes.experiment("bar"));
store.addEnrollment(experiment);
Assert.equal(
store.getExperimentForFeature("purple"),
experiment,
"should return a matching experiment for the given feature"
);
});
add_task(async function test_hasExperimentForFeature() {
const store = ExperimentFakes.store();
await store.init();
store.addEnrollment(
ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "green" },
},
})
);
store.addEnrollment(
ExperimentFakes.experiment("foo2", {
branch: {
slug: "variant",
feature: { featureId: "yellow" },
},
})
);
store.addEnrollment(
ExperimentFakes.experiment("bar_expired", {
active: false,
branch: {
slug: "variant",
feature: { featureId: "purple" },
},
})
);
Assert.equal(
store.hasExperimentForFeature(),
false,
"should return false if the input is empty"
);
Assert.equal(
store.hasExperimentForFeature(undefined),
false,
"should return false if the input is undefined"
);
Assert.equal(
store.hasExperimentForFeature("green"),
true,
"should return true if there is an experiment with any of the given groups"
);
Assert.equal(
store.hasExperimentForFeature("purple"),
false,
"should return false if there is a non-active experiment with the given groups"
);
});
add_task(async function test_getAll_getAllActiveExperiments() {
const store = ExperimentFakes.store();
await store.init();
["foo", "bar", "baz"].forEach(slug =>
store.addEnrollment(ExperimentFakes.experiment(slug, { active: false }))
);
store.addEnrollment(ExperimentFakes.experiment("qux", { active: true }));
Assert.deepEqual(
store.getAll().map(e => e.slug),
["foo", "bar", "baz", "qux"],
".getAll() should return all experiments"
);
Assert.deepEqual(
store.getAllActiveExperiments().map(e => e.slug),
["qux"],
".getAllActiveExperiments() should return all experiments that are active"
);
});
add_task(async function test_getAll_getAllActiveExperiments() {
const store = ExperimentFakes.store();
await store.init();
["foo", "bar", "baz"].forEach(slug =>
store.addEnrollment(ExperimentFakes.experiment(slug, { active: false }))
);
store.addEnrollment(ExperimentFakes.experiment("qux", { active: true }));
store.addEnrollment(ExperimentFakes.rollout("rol"));
Assert.deepEqual(
store.getAll().map(e => e.slug),
["foo", "bar", "baz", "qux", "rol"],
".getAll() should return all experiments and rollouts"
);
Assert.deepEqual(
store.getAllActiveExperiments().map(e => e.slug),
["qux"],
".getAllActiveExperiments() should return all experiments that are active and no rollouts"
);
});
add_task(async function test_getAllActiveRollouts() {
const store = ExperimentFakes.store();
await store.init();
["foo", "bar", "baz"].forEach(slug =>
store.addEnrollment(ExperimentFakes.rollout(slug))
);
store.addEnrollment(ExperimentFakes.experiment("qux", { active: true }));
Assert.deepEqual(
store.getAll().map(e => e.slug),
["foo", "bar", "baz", "qux"],
".getAll() should return all experiments and rollouts"
);
Assert.deepEqual(
store.getAllActiveRollouts().map(e => e.slug),
["foo", "bar", "baz"],
".getAllActiveRollouts() should return all rollouts"
);
});
add_task(async function test_addEnrollment_experiment() {
const store = ExperimentFakes.store();
const exp = ExperimentFakes.experiment("foo");
await store.init();
store.addEnrollment(exp);
Assert.equal(store.get("foo"), exp, "should save experiment by slug");
});
add_task(async function test_addEnrollment_rollout() {
const store = ExperimentFakes.store();
const rollout = ExperimentFakes.rollout("foo");
await store.init();
store.addEnrollment(rollout);
Assert.equal(store.get("foo"), rollout, "should save rollout by slug");
});
add_task(async function test_updateExperiment() {
const features = [{ featureId: "cfr" }];
const experiment = Object.freeze(
ExperimentFakes.experiment("foo", { features, active: true })
);
const store = ExperimentFakes.store();
await store.init();
store.addEnrollment(experiment);
store.updateExperiment("foo", { active: false });
const actual = store.get("foo");
Assert.equal(actual.active, false, "should change updated props");
Assert.deepEqual(
actual.branch.features,
features,
"should not update other props"
);
});
add_task(async function test_sync_access_before_init() {
cleanupStorePrefCache();
let store = ExperimentFakes.store();
Assert.equal(store.getAll().length, 0, "Start with an empty store");
const syncAccessExp = ExperimentFakes.experiment("foo", {
features: [{ featureId: "newtab" }],
});
await store.init();
store.addEnrollment(syncAccessExp);
let prefValue;
try {
prefValue = JSON.parse(
Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}newtab`)
);
} catch (e) {
Assert.ok(false, "Failed to parse pref value");
}
Assert.ok(prefValue, "Parsed stored experiment");
Assert.equal(prefValue.slug, syncAccessExp.slug, "Got back the experiment");
// New un-initialized store that should read the pref value
store = ExperimentFakes.store();
Assert.equal(
store.getExperimentForFeature("newtab").slug,
"foo",
"Returns experiment from pref"
);
});
add_task(async function test_sync_access_update() {
cleanupStorePrefCache();
let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", {
features: [{ featureId: "aboutwelcome" }],
});
await store.init();
store.addEnrollment(experiment);
store.updateExperiment("foo", {
branch: {
...experiment.branch,
features: [
{
featureId: "aboutwelcome",
value: { bar: "bar", enabled: true },
},
],
},
});
store = ExperimentFakes.store();
let cachedExperiment = store.getExperimentForFeature("aboutwelcome");
Assert.ok(cachedExperiment, "Got back 1 experiment");
Assert.deepEqual(
// `branch.feature` and not `features` because for sync access (early startup)
// experiments we only store the `isEarlyStartup` feature
cachedExperiment.branch.feature.value,
{ bar: "bar", enabled: true },
"Got updated value"
);
});
add_task(async function test_sync_features_only() {
cleanupStorePrefCache();
let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", {
features: [{ featureId: "cfr" }],
});
await store.init();
store.addEnrollment(experiment);
store = ExperimentFakes.store();
Assert.equal(store.getAll().length, 0, "cfr is not a sync access experiment");
});
add_task(async function test_sync_features_remotely() {
cleanupStorePrefCache();
let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", {
features: [{ featureId: "cfr", isEarlyStartup: true }],
});
await store.init();
store.addEnrollment(experiment);
store = ExperimentFakes.store();
Assert.ok(
Services.prefs.prefHasUserValue("nimbus.syncdatastore.cfr"),
"The cfr feature was stored as early access in prefs"
);
Assert.equal(store.getAll().length, 0, "Featre restored from prefs");
});
add_task(async function test_sync_access_unenroll() {
cleanupStorePrefCache();
let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", {
features: [{ featureId: "aboutwelcome" }],
active: true,
});
await store.init();
store.addEnrollment(experiment);
store.updateExperiment("foo", { active: false });
store = ExperimentFakes.store();
let experiments = store.getAll();
Assert.equal(experiments.length, 0, "Unenrolled experiment is deleted");
});
add_task(async function test_sync_access_unenroll_2() {
cleanupStorePrefCache();
let store = ExperimentFakes.store();
let experiment1 = ExperimentFakes.experiment("foo", {
features: [{ featureId: "newtab" }],
});
let experiment2 = ExperimentFakes.experiment("bar", {
features: [{ featureId: "aboutwelcome" }],
});
await store.init();
store.addEnrollment(experiment1);
store.addEnrollment(experiment2);
Assert.equal(store.getAll().length, 2, "2/2 experiments");
let other_store = ExperimentFakes.store();
Assert.ok(
other_store.getExperimentForFeature("aboutwelcome"),
"Fetches experiment from pref cache even before init (aboutwelcome)"
);
store.updateExperiment("bar", { active: false });
Assert.ok(
other_store.getExperimentForFeature("newtab").slug,
"Fetches experiment from pref cache even before init (newtab)"
);
Assert.ok(
!other_store.getExperimentForFeature("aboutwelcome")?.slug,
"Experiment was updated and should not be found"
);
store.updateExperiment("foo", { active: false });
Assert.ok(
!other_store.getExperimentForFeature("newtab")?.slug,
"Unenrolled from 2/2 experiments"
);
Assert.equal(
Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}newtab`, "").length,
0,
"Cleared pref 1"
);
Assert.equal(
Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}aboutwelcome`, "")
.length,
0,
"Cleared pref 2"
);
});
add_task(async function test_getRolloutForFeature_fromStore() {
const store = ExperimentFakes.store();
const rollout = ExperimentFakes.rollout("foo");
await store.init();
store.addEnrollment(rollout);
Assert.deepEqual(
store.getRolloutForFeature(rollout.featureIds[0]),
rollout,
"Should return back the same rollout"
);
});
add_task(async function test_getRolloutForFeature_fromSyncCache() {
let store = ExperimentFakes.store();
const rollout = ExperimentFakes.rollout("foo", {
branch: {
slug: "early-startup",
features: [{ featureId: "aboutwelcome", value: { enabled: true } }],
},
});
let updatePromise = new Promise(resolve =>
store.on(`update:${rollout.slug}`, resolve)
);
await store.init();
store.addEnrollment(rollout);
await updatePromise;
// New uninitialized store will return data from sync cache
// before init
store = ExperimentFakes.store();
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`),
"Sync cache is set"
);
Assert.equal(
store.getRolloutForFeature(rollout.featureIds[0]).slug,
rollout.slug,
"Should return back the same rollout"
);
Assert.deepEqual(
store.getRolloutForFeature(rollout.featureIds[0]).branch.feature,
rollout.branch.features[0],
"Should return back the same feature"
);
cleanupStorePrefCache();
});
add_task(async function test_remoteRollout() {
let store = ExperimentFakes.store();
const rollout = ExperimentFakes.rollout("foo", {
branch: {
slug: "early-startup",
features: [{ featureId: "aboutwelcome", value: { enabled: true } }],
},
});
let featureUpdateStub = sinon.stub();
let updatePromise = new Promise(resolve =>
store.on(`update:${rollout.slug}`, resolve)
);
store.on("featureUpdate:aboutwelcome", featureUpdateStub);
await store.init();
store.addEnrollment(rollout);
await updatePromise;
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`),
"Sync cache is set"
);
updatePromise = new Promise(resolve =>
store.on(`update:${rollout.slug}`, resolve)
);
store.updateExperiment(rollout.slug, { active: false });
// wait for it to be removed
await updatePromise;
Assert.ok(featureUpdateStub.calledTwice, "Called for add and remove");
Assert.ok(
store.get(rollout.slug),
"Rollout is still in the store just not active"
);
Assert.ok(
!store.getRolloutForFeature("aboutwelcome"),
"Feature rollout should not exist"
);
Assert.ok(
!Services.prefs.getStringPref(
`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`,
""
),
"Sync cache is cleared"
);
});
add_task(async function test_syncDataStore_setDefault() {
cleanupStorePrefCache();
const store = ExperimentFakes.store();
await store.init();
Assert.equal(
Services.prefs.getStringPref(
`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`,
""
),
"",
"Pref is empty"
);
let rollout = ExperimentFakes.rollout("foo", {
features: [{ featureId: "aboutwelcome", value: { remote: true } }],
});
store.addEnrollment(rollout);
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`),
"Stored in pref"
);
cleanupStorePrefCache();
});
add_task(async function test_syncDataStore_getDefault() {
cleanupStorePrefCache();
const store = ExperimentFakes.store();
const rollout = ExperimentFakes.rollout("aboutwelcome-slug", {
branch: {
features: [
{
featureId: "aboutwelcome",
value: { remote: true },
},
],
},
});
await store.init();
await store.addEnrollment(rollout);
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`)
);
let restoredRollout = store.getRolloutForFeature("aboutwelcome");
Assert.ok(restoredRollout);
Assert.ok(
restoredRollout.branch.features[0].value.remote,
"Restore data from pref"
);
cleanupStorePrefCache();
});
add_task(async function test_addEnrollment_rollout() {
const sandbox = sinon.createSandbox();
const store = ExperimentFakes.store();
const stub = sandbox.stub();
const value = { bar: true };
let rollout = ExperimentFakes.rollout("foo", {
features: [{ featureId: "aboutwelcome", value }],
});
store._onFeatureUpdate("aboutwelcome", stub);
await store.init();
store.addEnrollment(rollout);
Assert.deepEqual(
store.getRolloutForFeature("aboutwelcome"),
rollout,
"should return the stored value"
);
Assert.equal(stub.callCount, 1, "Called once on update");
Assert.equal(
stub.firstCall.args[1],
"rollout-updated",
"Called for correct reason"
);
});
add_task(async function test_storeValuePerPref_noVariables() {
const store = ExperimentFakes.store();
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [
{
// Ensure it gets saved to prefs
isEarlyStartup: true,
featureId: "purple",
},
],
},
});
await store.init();
store.addEnrollment(experiment);
let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`, ""),
"Experiment metadata saved to prefs"
);
Assert.equal(branch.getChildList("").length, 0, "No variables to store");
store._updateSyncStore({ ...experiment, active: false });
Assert.ok(
!Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`, ""),
"Experiment cleanup"
);
});
add_task(async function test_storeValuePerPref_withVariables() {
const store = ExperimentFakes.store();
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [
{
// Ensure it gets saved to prefs
isEarlyStartup: true,
featureId: "purple",
value: { color: "purple", enabled: true },
},
],
},
});
await store.init();
store.addEnrollment(experiment);
let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);
let val = Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`);
Assert.equal(
val.indexOf("color"),
-1,
`Experiment metadata does not contain variables ${val}`
);
Assert.equal(branch.getChildList("").length, 2, "Enabled and color");
store._updateSyncStore({ ...experiment, active: false });
Assert.ok(
!Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`, ""),
"Experiment cleanup"
);
Assert.equal(branch.getChildList("").length, 0, "Variables are also removed");
});
add_task(async function test_storeValuePerPref_returnsSameValue() {
let store = ExperimentFakes.store();
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [
{
// Ensure it gets saved to prefs
isEarlyStartup: true,
featureId: "purple",
value: { color: "purple", enabled: true },
},
],
},
});
await store.init();
store.addEnrollment(experiment);
let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);
store = ExperimentFakes.store();
const cachedExperiment = store.getExperimentForFeature("purple");
// Cached experiment format only stores early access feature
cachedExperiment.branch.features = [cachedExperiment.branch.feature];
delete cachedExperiment.branch.feature;
Assert.deepEqual(cachedExperiment, experiment, "Returns the same value");
// Cleanup
store._updateSyncStore({ ...experiment, active: false });
Assert.ok(
!Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`, ""),
"Experiment cleanup"
);
Assert.deepEqual(branch.getChildList(""), [], "Variables are also removed");
});
add_task(async function test_storeValuePerPref_returnsSameValue_allTypes() {
let store = ExperimentFakes.store();
// Add a fake feature that matches the variables we're testing
FeatureManifest.purple = {
variables: {
string: { type: "string" },
bool: { type: "boolean" },
array: { type: "json" },
number1: { type: "int" },
number2: { type: "int" },
number3: { type: "int" },
json: { type: "json" },
},
};
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [
{
// Ensure it gets saved to prefs
isEarlyStartup: true,
featureId: "purple",
value: {
string: "string",
bool: true,
array: [1, 2, 3],
number1: 42,
number2: 0,
number3: -5,
json: { jsonValue: true },
},
},
],
},
});
await store.init();
store.addEnrollment(experiment);
let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);
store = ExperimentFakes.store();
Assert.deepEqual(
store.getExperimentForFeature("purple").branch.feature.value,
experiment.branch.features[0].value,
"Returns the same value"
);
// Cleanup
store._updateSyncStore({ ...experiment, active: false });
Assert.ok(
!Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`, ""),
"Experiment cleanup"
);
Assert.deepEqual(branch.getChildList(""), [], "Variables are also removed");
delete FeatureManifest.purple;
});
add_task(async function test_cleanupOldRecipes() {
let store = ExperimentFakes.store();
let sandbox = sinon.createSandbox();
let stub = sandbox.stub(store, "_removeEntriesByKeys");
const experiment1 = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
features: [{ featureId: "purple" }],
},
});
const experiment2 = ExperimentFakes.experiment("bar", {
branch: {
slug: "variant",
features: [{ featureId: "purple" }],
},
});
const experiment3 = ExperimentFakes.experiment("baz", {
branch: {
slug: "variant",
features: [{ featureId: "purple" }],
},
});
const experiment4 = ExperimentFakes.experiment("faz", {
branch: {
slug: "variant",
features: [{ featureId: "purple" }],
},
});
// Exp 2 is kept because it's recent (even though it's not active)
// Exp 4 is kept because it's active
experiment2.lastSeen = new Date().toISOString();
experiment2.active = false;
experiment1.lastSeen = new Date("2020-01-01").toISOString();
experiment1.active = false;
experiment3.active = false;
delete experiment3.lastSeen;
store._data = {
foo: experiment1,
bar: experiment2,
baz: experiment3,
faz: experiment4,
};
store._cleanupOldRecipes();
Assert.ok(stub.calledOnce, "Recipe cleanup called");
Assert.equal(
stub.firstCall.args[0].length,
2,
"We call to remove enrollments"
);
Assert.equal(
stub.firstCall.args[0][0],
experiment1.slug,
"Should remove expired enrollment"
);
Assert.equal(
stub.firstCall.args[0][1],
experiment3.slug,
"Should remove invalid enrollment"
);
});