gecko-dev/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js

3152 lines
83 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const {
_ExperimentFeature: ExperimentFeature,
NimbusFeatures,
} = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
const { PrefUtils } = ChromeUtils.import(
"resource://normandy/lib/PrefUtils.jsm"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const { TelemetryEvents } = ChromeUtils.import(
"resource://normandy/lib/TelemetryEvents.jsm"
);
/**
* Pick a single entry from an object and return a new object containing only
* that entry.
*
* @param {object} obj The object to copy the value from.
* @param {string} key The key whose value is to be copied.
*
* @returns {object} An object with the property `key` set to `obj[key]`.
*/
function pick(obj, key) {
return { [key]: obj[key] };
}
const PREF_FEATURES = [
new ExperimentFeature("test-set-pref", {
description: "Test feature that sets a pref on the default branch.",
owner: "test@test.test",
hasExposure: false,
isEarlyStartup: false,
variables: {
foo: {
type: "string",
description: "Test variable",
setPref: "nimbus.test-only.foo",
},
},
}),
new ExperimentFeature("test-set-user-pref", {
description: "Test feature that sets a pref on the user branch.",
owner: "test@test.test",
hasExposure: false,
isEarlyStartup: true,
variables: {
bar: {
type: "string",
description: "Test variable",
setPref: "nimbus.test-only.bar",
},
},
}),
];
const DEFAULT_VALUE = "default-value";
const USER_VALUE = "user-value";
const EXPERIMENT_VALUE = "experiment-value";
const ROLLOUT_VALUE = "rollout-value";
const OVERWRITE_VALUE = "overwrite-value";
const USER = "user";
const DEFAULT = "default";
const ROLLOUT = "rollout";
const EXPERIMENT = "experiment";
const PREFS = {
[DEFAULT]: "nimbus.test-only.foo",
[USER]: "nimbus.test-only.bar",
};
const FEATURE_IDS = {
[DEFAULT]: "test-set-pref",
[USER]: "test-set-user-pref",
};
const CONFIGS = {
[DEFAULT]: {
[ROLLOUT]: {
featureId: FEATURE_IDS[DEFAULT],
value: {
foo: ROLLOUT_VALUE,
},
},
[EXPERIMENT]: {
featureId: FEATURE_IDS[DEFAULT],
value: {
foo: EXPERIMENT_VALUE,
},
},
},
[USER]: {
[ROLLOUT]: {
featureId: FEATURE_IDS[USER],
value: {
bar: ROLLOUT_VALUE,
},
},
[EXPERIMENT]: {
featureId: FEATURE_IDS[USER],
value: {
bar: EXPERIMENT_VALUE,
},
},
},
};
/**
* Set the given pref values on their respective branches (if they are not
* null).
*/
function setPrefs(pref, { defaultBranchValue = null, userBranchValue = null }) {
if (defaultBranchValue !== null) {
Services.prefs
.getDefaultBranch(null)
.setStringPref(pref, defaultBranchValue);
}
if (userBranchValue !== null) {
Services.prefs.setStringPref(pref, userBranchValue);
}
}
function assertExpectedPrefValues(pref, branch, expected, visible, msg) {
info(`Assert pref ${pref} on branch ${branch} matches ${expected} ${msg}`);
const hasBranchValue = expected !== null;
const hasVisibleValue = visible !== null;
function hasValueMsg(hasValue) {
return `Expected pref "${pref}" on the ${branch} branch to${
hasValue ? " " : " not "
}have a value ${msg}`;
}
let prefBranch;
switch (branch) {
case USER:
Assert.equal(
Services.prefs.prefHasUserValue(pref),
hasBranchValue,
hasValueMsg(hasBranchValue)
);
prefBranch = Services.prefs;
break;
case DEFAULT:
Assert.equal(
Services.prefs.prefHasDefaultValue(pref),
hasBranchValue,
hasValueMsg(hasBranchValue)
);
prefBranch = Services.prefs.getDefaultBranch(null);
break;
default:
Assert.ok(false, "invalid pref branch");
}
if (hasBranchValue) {
Assert.equal(
prefBranch.getStringPref(pref),
expected,
`Expected pref "${pref} on the ${branch} branch to be ${JSON.stringify(
expected
)} ${msg}`
);
}
if (hasVisibleValue) {
Assert.equal(
Services.prefs.getStringPref(pref),
visible,
`Expected pref "${pref}" to be ${JSON.stringify(visible)} ${msg}`
);
} else {
Assert.ok(
!Services.prefs.prefHasUserValue(pref) &&
!Services.prefs.prefHasDefaultValue(pref),
`Expected pref "${pref} to not be set ${msg}`
);
}
}
/**
* Assert the store has no active experiments or rollouts.
*/
function assertEmptyStore(store) {
Assert.deepEqual(
store.getAllActiveExperiments(),
[],
"There should be no experiments active."
);
Assert.deepEqual(
store.getAllActiveRollouts(),
[],
"There should be no rollouts active"
);
}
/**
* Remove the experiment store.
*/
async function cleanupStore(store) {
// We need to call finalize first to ensure that any pending saves from
// JSONFile.saveSoon overwrite files on disk.
await store._store.finalize();
await IOUtils.remove(store._store.path);
}
/**
* Assert the manager has no active pref observers.
*/
function assertNoObservers(manager) {
Assert.equal(
manager._prefs.size,
0,
"There should be no active pref observers"
);
Assert.equal(
manager._prefsBySlug.size,
0,
"There should be no active pref observers"
);
}
/**
* Remove all pref observers on the given ExperimentManager.
*/
function removePrefObservers(manager) {
for (const [name, entry] of manager._prefs.entries()) {
Services.prefs.removeObserver(name, entry.observer);
}
manager._prefs.clear();
manager._prefsBySlug.clear();
}
add_setup(function setup() {
do_get_profile();
Services.fog.initializeFOG();
const cleanupFeatures = ExperimentTestUtils.addTestFeatures(...PREF_FEATURES);
registerCleanupFunction(cleanupFeatures);
});
add_task(async function test_enroll_setPref_rolloutsAndExperiments() {
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
assertEmptyStore(store);
/**
* Test that prefs are set correctly before and after enrollment and
* unenrollment.
*
* @param {object} options
* @param {string} options.pref
* The name of the pref.
*
* @param {string} options.branch
* The name of the pref branch ("user" or "default").
*
* @param {object} options.configs
* The rollout and experiment feature configurations.
*
* @param {string?} options.defaultBranchValue
* An optional value to set for the pref on the default branch
* before the first enrollment.
*
* @param {string?} options.userBranchValue
* An optional value to set for the pref on the user branch
* before the first enrollment.
*
* @param {string[]} options.enrollOrder
* The order to do the enrollments. Must only contain
* "experiment" and "rollout" as values.
*
* @param {string[]} options.unenrollOrder
* The order to undo the enrollments. Must only contain
* "experiment" and "rollout" as values.
*
* @param {(string|null)[]} options.expectedValues
* The expected values of the preft on the given branch at each point:
*
* * before enrollment;
* * one entry each each after enrolling in `options.enrollOrder[i]`; and
* * one entry each each after unenrolling in `options.unenrollOrder[i]`.
*
* A value of null indicates that the pref should not be set on that
* branch.
*
* @param {(string|null)[]?} options.visibleValues
* The expected values returned by
* `Services.prefs.getStringPref` (i.e., the user branch if set,
* falling back to the default branch if not), in the same
* order as |options.expectedValues|.
*
* If undefined, then it will default `options.expectedValues`.
*/
async function doBaseTest({
pref,
branch,
configs,
userBranchValue = undefined,
defaultBranchValue = undefined,
enrollOrder,
unenrollOrder,
expectedValues,
visibleValues = undefined,
}) {
if (visibleValues === undefined) {
visibleValues = expectedValues;
}
const cleanupFns = {};
let i = 0;
setPrefs(pref, { defaultBranchValue, userBranchValue });
assertExpectedPrefValues(
pref,
branch,
expectedValues[i],
visibleValues[i],
"before enrollment"
);
i++;
for (const enrollmentKind of enrollOrder) {
const isRollout = enrollmentKind === ROLLOUT;
cleanupFns[
enrollmentKind
] = await ExperimentFakes.enrollWithFeatureConfig(
configs[enrollmentKind],
{ manager, isRollout }
);
assertExpectedPrefValues(
pref,
branch,
expectedValues[i],
visibleValues[i],
`after ${enrollmentKind} enrollment`
);
i++;
}
for (const enrollmentKind of unenrollOrder) {
await cleanupFns[enrollmentKind]();
assertExpectedPrefValues(
pref,
branch,
expectedValues[i],
visibleValues[i],
`after ${enrollmentKind} unenrollment`
);
i++;
}
assertEmptyStore(store);
Services.prefs.deleteBranch(pref);
}
// Tests for a feature that would set a pref on the default branch, but the variable is omitted.
{
const branch = DEFAULT;
const pref = PREFS[branch];
const configs = {
[ROLLOUT]: {
featureId: FEATURE_IDS[DEFAULT],
value: {},
},
[EXPERIMENT]: {
featureId: FEATURE_IDS[DEFAULT],
value: {},
},
};
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enroll in a rollout then unenroll.
await doTest({
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, null, null],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, null, null],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
// Enroll in an experiment then unenroll.
await doTest({
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, null, null],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, null, null],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
}
// Test for a feature that would set a pref on the user branch, but the variable is omitted.
{
const branch = USER;
const pref = PREFS[branch];
const configs = {
[ROLLOUT]: {
featureId: FEATURE_IDS[DEFAULT],
value: {},
},
[EXPERIMENT]: {
featureId: FEATURE_IDS[DEFAULT],
value: {},
},
};
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enroll in a rollout then unenroll.
await doTest({
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, null, null],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, null, null],
visibleValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
// Enroll in an experiment then unenroll.
await doTest({
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, null, null],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, null, null],
visibleValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
}
// Tests for a feature that sets prefs on the default branch.
{
const branch = DEFAULT;
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enroll in rollout then unenroll.
await doTest({
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, ROLLOUT_VALUE, ROLLOUT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [DEFAULT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, ROLLOUT_VALUE, ROLLOUT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [DEFAULT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
// Enroll in experiment then unenroll.
await doTest({
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, EXPERIMENT_VALUE, EXPERIMENT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [DEFAULT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, EXPERIMENT_VALUE, EXPERIMENT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [DEFAULT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
// Enroll in rollout then experiment; unenroll in reverse order.
await doTest({
configs,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
ROLLOUT_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
DEFAULT_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
DEFAULT_VALUE,
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
ROLLOUT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
DEFAULT_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
DEFAULT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
// Enroll in rollout then experiment; unenroll in same order.
await doTest({
configs,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
DEFAULT_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
DEFAULT_VALUE,
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
DEFAULT_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
DEFAULT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
// Enroll in experiment then rollout; unenroll in reverse order.
await doTest({
configs,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
DEFAULT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
DEFAULT_VALUE,
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
DEFAULT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
DEFAULT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
// Enroll in experiment then rollout; unenroll in same order.
await doTest({
configs,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
ROLLOUT_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
DEFAULT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
DEFAULT_VALUE,
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
ROLLOUT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
DEFAULT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
DEFAULT_VALUE,
],
visibleValues: [
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
USER_VALUE,
],
});
}
// Tests for a feature that sets prefs on the user branch.
{
const branch = USER;
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enroll in rollout then unenroll.
await doTest({
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, ROLLOUT_VALUE, null],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [null, ROLLOUT_VALUE, null],
visibleValues: [DEFAULT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [USER_VALUE, ROLLOUT_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
enrollOrder: [ROLLOUT],
unenrollOrder: [ROLLOUT],
expectedValues: [USER_VALUE, ROLLOUT_VALUE, USER_VALUE],
});
// Enroll in experiment then unenroll.
await doTest({
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, EXPERIMENT_VALUE, null],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [null, EXPERIMENT_VALUE, null],
visibleValues: [DEFAULT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [USER_VALUE, EXPERIMENT_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
enrollOrder: [EXPERIMENT],
unenrollOrder: [EXPERIMENT],
expectedValues: [USER_VALUE, EXPERIMENT_VALUE, USER_VALUE],
});
// Enroll in rollout then experiment; unenroll in reverse order.
await doTest({
configs,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
null,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
null,
],
visibleValues: [
DEFAULT_VALUE, // User branch falls back to default branch.
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
DEFAULT_VALUE, // User branch falls back to default branch.
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
USER_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
USER_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
USER_VALUE,
],
});
// Enroll in rollout then experiment; unenroll in same order.
await doTest({
configs,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
null,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
null,
],
visibleValues: [
DEFAULT_VALUE, // User branch falls back to default branch.
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
DEFAULT_VALUE, // User branch falls back to default branch.
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
USER_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
enrollOrder: [ROLLOUT, EXPERIMENT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
USER_VALUE,
ROLLOUT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
USER_VALUE,
],
});
// Enroll in experiment then rollout; unenroll in reverse order.
await doTest({
configs,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
null,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
null,
],
visibleValues: [
DEFAULT_VALUE, // User branch falls back to default branch.
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
DEFAULT_VALUE, // User branch falls back to default branch.
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
USER_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [
USER_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
USER_VALUE,
],
});
// Enroll in experiment then rollout; unenroll in same order.
await doTest({
configs,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
null,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
null,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
null,
],
visibleValues: [
DEFAULT_VALUE, // User branch falls back to default branch.
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
DEFAULT_VALUE, // User branch falls back to default branch.
],
});
await doTest({
configs,
userBranchValue: USER_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
USER_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
USER_VALUE,
],
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
enrollOrder: [EXPERIMENT, ROLLOUT],
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [
USER_VALUE,
EXPERIMENT_VALUE,
EXPERIMENT_VALUE,
ROLLOUT_VALUE,
USER_VALUE,
],
});
}
await cleanupStore(store);
});
add_task(async function test_restorePrefs_experimentAndRollout() {
/**
* Test that prefs are set correctly after restoring from a saved store file
* and unrnollment.
*
* This test sets up some enrollments and saves them to disk.
*
* A browser restart will be simulated by creating a new ExperimentStore and
* ExperimentManager to restore the saved enrollments.
*
* @param {object} options
* @param {string} options.pref
* The name of the pref.
*
* @param {string} options.branch
* The name of the pref branch ("user" or "default").
*
* @param {object} options.configs
* The rollout and experiment feature configurations.
*
* @param {string?} options.defaultBranchValue
* An optional value to set for the pref on the default branch
* before the first enrollment.
*
* @param {string?} options.userBranchValue
* An optional value to set for the pref on the user branch
* before the first enrollment.
*
* @param {string[]} options.unenrollOrder
* An optional value to set for the pref on the default branch
* before the first enrollment.
*
* @param {(string|null)[]} options.expectedValues
* The expected values of the preft on the given branch at each point:
*
* * before enrollment;
* * one entry each each after enrolling in `options.enrollOrder[i]`; and
* * one entry each each after unenrolling in `options.unenrollOrder[i]`.
*
* A value of null indicates that the pref should not be set on that
* branch.
*
* @param {(string|null)[]?} options.visibleValues
* The expected values returned by
* Services.prefs.getStringPref (i.e., the user branch if set,
* falling back to the default branch if not), in the same
* order as `options.expectedValues`.
*
* If undefined, then it will default to `options.expectedValues`.
*/
async function doBaseTest({
featureId,
pref,
branch,
configs,
defaultBranchValue = null,
userBranchValue = null,
unenrollOrder,
expectedValues,
visibleValues = undefined,
}) {
if (![USER, DEFAULT].includes(branch)) {
Assert.ok(false, `invalid branch ${branch}`);
}
if (visibleValues === undefined) {
visibleValues = expectedValues;
}
// Set the initial conditions.
setPrefs(pref, { defaultBranchValue, userBranchValue });
// Enroll in some experiments and save the state to disk.
{
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
assertEmptyStore(store);
for (const [enrollmentKind, config] of Object.entries(configs)) {
await ExperimentFakes.enrollWithFeatureConfig(config, {
manager,
isRollout: enrollmentKind === ROLLOUT,
});
}
store._store.saveSoon();
await store._store.finalize();
// User branch prefs persist through restart, so we only want to delete
// the prefs if we changed the default branch.
if (branch === "default") {
Services.prefs.deleteBranch(pref);
}
removePrefObservers(manager);
assertNoObservers(manager);
}
// Restore the default branch value as it was before "restarting".
setPrefs(pref, { defaultBranchValue });
// If this is not a user branch pref, restore the user branch value. User
// branch values persist through restart, so we don't want to overwrite a
// value we just set.
if (branch === "default") {
setPrefs(pref, { userBranchValue });
}
const sandbox = sinon.createSandbox();
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
const setPrefSpy = sandbox.spy(PrefUtils, "setPref");
await manager.onStartup();
if (branch === DEFAULT) {
Assert.ok(setPrefSpy.calledOnce, "Should have called setPref once total");
Assert.ok(
setPrefSpy.calledOnceWith(pref, expectedValues[0], { branch }),
`Should have only called setPref with correct args (called with: ${JSON.stringify(
setPrefSpy.getCall(0).args
)}) expected ${JSON.stringify([pref, expectedValues[0], { branch }])})`
);
} else if (branch === USER) {
Assert.ok(
setPrefSpy.notCalled,
"Should have not called setPref for a user branch pref"
);
}
assertExpectedPrefValues(
pref,
branch,
expectedValues[0],
visibleValues[0],
"after manager startup"
);
const slugs = {
[ROLLOUT]: store.getRolloutForFeature(featureId)?.slug,
[EXPERIMENT]: store.getExperimentForFeature(featureId)?.slug,
};
let i = 1;
for (const enrollmentKind of unenrollOrder) {
manager.unenroll(slugs[enrollmentKind]);
assertExpectedPrefValues(
pref,
branch,
expectedValues[i],
visibleValues[i],
`after ${enrollmentKind} unenrollment`
);
i++;
}
for (const enrollmentKind of unenrollOrder) {
// The unenrollment happened normally, not through a cleanup function.
store._deleteForTests(slugs[enrollmentKind]);
}
assertNoObservers(manager);
assertEmptyStore(store);
await cleanupStore(store);
Services.prefs.deleteBranch(pref);
sandbox.restore();
}
{
const branch = DEFAULT;
const featureId = FEATURE_IDS[branch];
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args => doBaseTest({ featureId, pref, branch, ...args });
// Tests with no prefs set beforehand.
await doTest({
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE],
});
await doTest({
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, ROLLOUT_VALUE],
});
await doTest({
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, ROLLOUT_VALUE],
});
await doTest({
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, EXPERIMENT_VALUE],
});
// Tests where the default branch is set beforehand.
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, DEFAULT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, DEFAULT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE],
});
// Tests where the user branch is set beforehand.
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, ROLLOUT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, ROLLOUT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, EXPERIMENT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
// Tests with both branches set beforehand
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE],
visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE],
});
}
{
const branch = USER;
const featureId = FEATURE_IDS[branch];
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args =>
doBaseTest({ featureId, pref, branch, configs, ...args });
// Tests with no prefs set beforehand.
await doTest({
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, null],
});
await doTest({
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, null],
});
await doTest({
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, null],
});
await doTest({
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, null],
});
// Tests with the default branch set beforehand.
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, null],
visibleValues: [EXPERIMENT_VALUE, DEFAULT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, null],
visibleValues: [ROLLOUT_VALUE, DEFAULT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, null],
visibleValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, null],
visibleValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE],
});
// Tests with the user branch set beforehand.
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, USER_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, USER_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, USER_VALUE],
});
await doTest({
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, USER_VALUE],
});
// Tests with both branches set beforehand
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, EXPERIMENT),
unenrollOrder: [EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs: pick(configs, ROLLOUT),
unenrollOrder: [ROLLOUT],
expectedValues: [ROLLOUT_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [EXPERIMENT, ROLLOUT],
expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, USER_VALUE],
});
await doTest({
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
configs,
unenrollOrder: [ROLLOUT, EXPERIMENT],
expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, USER_VALUE],
});
}
});
add_task(async function test_prefChange() {
/**
* Test that pref tampering causes unenrollment.
*
* This test sets up some enrollments and then modifies the given `pref` on a
* branch specified by `setBranch` and checks that unenrollments happen as
* appropriate.
*
* @param {object} options
*
* @param {string} options.pref
* The name of the pref.
*
* @param {string?} options.defaultBranchValue
* An optional value to set for the pref on the default branch
* before the first enrollment.
*
* @param {string?} options.userBranchValue
* An optional value to set for the pref on the user branch
* before the first enrollment.
*
* @param {object} options.configs
* The rollout and experiment feature configurations.
*
* @param {string} options.setBranch
* The branch that the test will set (either "user" or "default").
*
* @param {string[]} options.expectedEnrollments
* The list of enrollment kinds (e.g., "rollout" or "experiment") that
* should be active after setting the pref on the requested branch.
*
* @param {string} options.expectedDefault
* The expected value of the default branch after setting the pref on
* the requested branch.
*
* A value of null indicates that the pref should not be set on the
* default branch.
*
* @param {string} options.expectedUser
* The expected value of the user branch after setting the pref on the
* requested branch.
*
* A value of null indicates that the pref should not be set on the
* user branch.
*/
async function doBaseTest({
pref,
defaultBranchValue = null,
userBranchValue = null,
configs,
setBranch,
expectedEnrollments = [],
expectedDefault = null,
expectedUser = null,
}) {
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
const cleanup = {};
const slugs = {};
await manager.onStartup();
assertEmptyStore(store);
setPrefs(pref, { defaultBranchValue, userBranchValue });
info(`Enrolling in ${Array.from(Object.keys(configs)).join(", ")} ...`);
for (const [enrollmentKind, config] of Object.entries(configs)) {
const isRollout = enrollmentKind === ROLLOUT;
cleanup[enrollmentKind] = await ExperimentFakes.enrollWithFeatureConfig(
config,
{
manager,
isRollout,
}
);
const enrollments = isRollout
? store.getAllActiveRollouts()
: store.getAllActiveExperiments();
Assert.equal(
enrollments.length,
1,
`Expected one ${enrollmentKind} enrollment`
);
slugs[enrollmentKind] = enrollments[0].slug;
}
info(
`Overwriting ${pref} with "${OVERWRITE_VALUE}" on ${setBranch} branch`
);
PrefUtils.setPref(pref, OVERWRITE_VALUE, { branch: setBranch });
if (expectedDefault === null) {
Assert.ok(
!Services.prefs.prefHasDefaultValue(pref),
`Expected the default branch not to be set for ${pref}`
);
} else {
Assert.equal(
Services.prefs.getDefaultBranch(null).getStringPref(pref),
expectedDefault,
`Expected the value of ${pref} on the default branch to match the expected value`
);
}
if (expectedUser === null) {
Assert.ok(
!Services.prefs.prefHasUserValue(pref),
`Expected the user branch not to be set for ${pref}`
);
} else {
Assert.equal(
Services.prefs.getStringPref(pref),
expectedUser,
`Expected the value of ${pref} on the user branch to match the expected value`
);
}
for (const enrollmentKind of expectedEnrollments) {
const enrollment = store.get(slugs[enrollmentKind]);
Assert.ok(
enrollment !== null,
`An enrollment of kind ${enrollmentKind} should exist`
);
Assert.ok(enrollment.active, "It should still be active");
}
for (const enrollmentKind of Object.keys(configs)) {
if (!expectedEnrollments.includes(enrollmentKind)) {
const slug = slugs[enrollmentKind];
const enrollment = store.get(slug);
Assert.ok(
enrollment !== null,
`An enrollment of kind ${enrollmentKind} should exist`
);
Assert.ok(!enrollment.active, "It should not be active");
store._deleteForTests(slug);
}
}
for (const enrollmentKind of expectedEnrollments) {
await cleanup[enrollmentKind]();
}
assertNoObservers(manager);
assertEmptyStore(store);
await cleanupStore(store);
Services.prefs.deleteBranch(pref);
}
{
const branch = DEFAULT;
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enrolled in rollout, set default branch.
await doTest({
configs: pick(configs, ROLLOUT),
setBranch: DEFAULT,
expectedDefault: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: DEFAULT,
expectedDefault: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: USER_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: USER_VALUE,
});
// Enrolled in rollout, set user branch.
await doTest({
configs: pick(configs, ROLLOUT),
setBranch: USER,
expectedDefault: ROLLOUT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: ROLLOUT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
// Enrolled in experiment, set default branch.
await doTest({
configs: pick(configs, EXPERIMENT),
setBranch: DEFAULT,
expectedDefault: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: DEFAULT,
expectedDefault: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: USER_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: USER_VALUE,
});
// Enrolled in experiment, set user branch.
await doTest({
configs: pick(configs, EXPERIMENT),
setBranch: USER,
expectedDefault: EXPERIMENT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: EXPERIMENT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
// Enroll in both, set default branch.
await doTest({
configs,
setBranch: DEFAULT,
expectedDefault: OVERWRITE_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
setBranch: DEFAULT,
expectedDefault: OVERWRITE_VALUE,
});
await doTest({
configs,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT, ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: USER_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT, ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: USER_VALUE,
});
// Enroll in both, set user branch.
await doTest({
configs,
setBranch: USER,
expectedDefault: EXPERIMENT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: EXPERIMENT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
}
{
const branch = USER;
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enrolled in rollout, set default branch.
await doTest({
configs: pick(configs, ROLLOUT),
setBranch: DEFAULT,
expectedEnrollments: [ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: ROLLOUT_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: ROLLOUT_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: ROLLOUT_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: ROLLOUT_VALUE,
});
// Enrolled in rollout, set user branch.
await doTest({
configs: pick(configs, ROLLOUT),
setBranch: USER,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
userBranchValue: USER_VALUE,
setBranch: USER,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
// Enrolled in experiment, set default branch.
await doTest({
configs: pick(configs, EXPERIMENT),
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
// Enrolled in experiment, set user branch.
await doTest({
configs: pick(configs, EXPERIMENT),
setBranch: USER,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
userBranchValue: USER_VALUE,
setBranch: USER,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
// Enrolled in both, set default branch.
await doTest({
configs,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT, ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT, ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
await doTest({
configs,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT, ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: DEFAULT,
expectedEnrollments: [EXPERIMENT, ROLLOUT],
expectedDefault: OVERWRITE_VALUE,
expectedUser: EXPERIMENT_VALUE,
});
// Enrolled in both, set user branch.
await doTest({
configs,
setBranch: USER,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedUser: OVERWRITE_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
setBranch: USER,
expectedDefault: DEFAULT_VALUE,
expectedUser: OVERWRITE_VALUE,
});
}
});
add_task(async function test_deleteBranch() {
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
assertEmptyStore(store);
const cleanup = [];
cleanup.push(
await ExperimentFakes.enrollWithFeatureConfig(CONFIGS[USER][EXPERIMENT], {
manager,
}),
await ExperimentFakes.enrollWithFeatureConfig(CONFIGS[USER][ROLLOUT], {
manager,
isRollout: true,
}),
await ExperimentFakes.enrollWithFeatureConfig(
CONFIGS[DEFAULT][EXPERIMENT],
{ manager }
),
await ExperimentFakes.enrollWithFeatureConfig(CONFIGS[DEFAULT][ROLLOUT], {
manager,
isRollout: true,
})
);
Services.prefs.deleteBranch(PREFS[USER]);
Services.prefs.deleteBranch(PREFS[DEFAULT]);
// deleteBranch does not trigger pref observers!
Assert.equal(
store.getAll().length,
4,
"nsIPrefBranch::deleteBranch does not trigger unenrollment"
);
for (const cleanupFn of cleanup) {
await cleanupFn();
}
assertNoObservers(manager);
assertEmptyStore(store);
await cleanupStore(store);
});
add_task(async function test_clearUserPref() {
/**
* Test that nsIPrefBranch::clearUserPref() correctly interacts with pref
* tampering logic.
*
* This test sets up some enrollments and then clears the pref specified and
* checks that unenrollments happen as * appropriate.
*
* @param {object} options
*
* @param {string} options.pref
* The name of the pref.
*
* @param {string?} options.defaultBranchValue
* An optional value to set for the pref on the default branch
* before the first enrollment.
*
* @param {string?} options.userBranchValue
* An optional value to set for the pref on the user branch
* before the first enrollment.
*
* @param {object} options.configs
* The rollout and experiment feature configurations.
*
* @param {boolean} options.expectedEnrolled
* Whether or not the enrollments defined in `configs` should still be
* active after clearing the user branch.
*
* @param {string} options.expectedDefault
* The expected value of the default branch after clearing the user branch.
*
* A value of null indicates that the pref should not be set on the default
* branch.
*/
async function doBaseTest({
pref,
defaultBranchValue = null,
userBranchValue = null,
configs,
expectedEnrolled,
expectedDefault = null,
}) {
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
assertEmptyStore(store);
const cleanup = [];
const slugs = {};
setPrefs(pref, { defaultBranchValue, userBranchValue });
for (const [enrollmentKind, config] of Object.entries(configs)) {
const isRollout = enrollmentKind === ROLLOUT;
cleanup.push(
await ExperimentFakes.enrollWithFeatureConfig(config, {
manager,
isRollout,
})
);
const enrollments = isRollout
? store.getAllActiveRollouts()
: store.getAllActiveExperiments();
Assert.equal(
enrollments.length,
1,
`Expected one ${enrollmentKind} enrollment`
);
slugs[enrollmentKind] = enrollments[0].slug;
}
Services.prefs.clearUserPref(pref);
for (const enrollmentKind of Object.keys(configs)) {
const slug = slugs[enrollmentKind];
const enrollment = store.get(slug);
Assert.ok(
enrollment !== null,
`An enrollment of kind ${enrollmentKind} should exist`
);
if (expectedEnrolled) {
Assert.ok(enrollment.active, "It should be active");
} else {
Assert.ok(!enrollment.active, "It should not be active");
}
}
if (expectedDefault === null) {
Assert.ok(
!Services.prefs.prefHasDefaultValue(pref),
`Expected the default branch not to be set for ${pref}`
);
} else {
Assert.equal(
Services.prefs.getDefaultBranch(null).getStringPref(pref),
expectedDefault,
`Expected the value of ${pref} on the default branch to match the expected value`
);
}
Assert.ok(
!Services.prefs.prefHasUserValue(pref),
`Expected the user branch not to be set for ${pref}`
);
if (expectedEnrolled) {
for (const cleanupFn of Object.values(cleanup)) {
await cleanupFn();
}
} else {
for (const slug of Object.values(slugs)) {
store._deleteForTests(slug);
}
}
assertNoObservers(manager);
assertEmptyStore(store);
await cleanupStore(store);
Services.prefs.deleteBranch(pref);
}
{
const branch = DEFAULT;
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args => doBaseTest({ pref, branch, ...args });
// Enroll in rollout.
await doTest({
configs: pick(configs, ROLLOUT),
expectedEnrolled: true,
expectedDefault: ROLLOUT_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
expectedEnrolled: true,
expectedDefault: ROLLOUT_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
userBranchValue: USER_VALUE,
expectedEnrolled: false,
expectedDefault: ROLLOUT_VALUE,
});
await doTest({
configs: pick(configs, ROLLOUT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
expectedEnrolled: false,
expectedDefault: DEFAULT_VALUE,
});
// Enroll in experiment.
await doTest({
configs: pick(configs, EXPERIMENT),
expectedEnrolled: true,
expectedDefault: EXPERIMENT_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
expectedEnrolled: true,
expectedDefault: EXPERIMENT_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
userBranchValue: USER_VALUE,
expectedEnrolled: false,
expectedDefault: EXPERIMENT_VALUE,
});
await doTest({
configs: pick(configs, EXPERIMENT),
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
expectedEnrolled: false,
expectedDefault: DEFAULT_VALUE,
});
// Enroll in both.
await doTest({
configs,
expectedEnrolled: true,
expectedDefault: EXPERIMENT_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
expectedEnrolled: true,
expectedDefault: EXPERIMENT_VALUE,
});
await doTest({
configs,
userBranchValue: USER_VALUE,
expectedEnrolled: false,
expectedDefault: EXPERIMENT_VALUE,
});
await doTest({
configs,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
expectedEnrolled: false,
expectedDefault: DEFAULT_VALUE,
});
}
{
const branch = USER;
const pref = PREFS[branch];
const configs = CONFIGS[branch];
const doTest = args =>
doBaseTest({ pref, branch, expectedEnrolled: false, ...args });
// Because this pref is set on the user branch, clearing the user branch has
// the same effect for every suite of configs.
for (const selectedConfig of [
pick(configs, ROLLOUT),
pick(configs, EXPERIMENT),
configs,
]) {
await doTest({
configs: selectedConfig,
});
await doTest({
configs: selectedConfig,
defaultBranchValue: DEFAULT_VALUE,
expectedDefault: DEFAULT_VALUE,
});
await doTest({
configs: selectedConfig,
userBranchValue: USER_VALUE,
});
await doTest({
configs: selectedConfig,
defaultBranchValue: DEFAULT_VALUE,
userBranchValue: USER_VALUE,
expectedDefault: DEFAULT_VALUE,
});
}
}
});
// Test that unenrollment doesn't happen if a pref changes but it wasn't set.
add_task(async function test_prefChanged_noPrefSet() {
const featureId = "test-set-pref-2";
const pref = "nimbus.test-only.baz";
function featureFactory(isEarlyStartup) {
return new ExperimentFeature(featureId, {
description: "Test feature that sets a pref",
owner: "test@test.test",
hasExposure: false,
isEarlyStartup,
variables: {
baz: {
type: "string",
description: "Test variable",
setPref: pref,
},
qux: {
type: "string",
description: "Test variable",
},
},
});
}
const config = {
featureId,
value: {
qux: "qux",
},
};
for (const isEarlyStartup of [true, false]) {
const feature = featureFactory(isEarlyStartup);
const cleanupFeature = ExperimentTestUtils.addTestFeatures(feature);
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
for (const branch of [USER, DEFAULT]) {
for (const defaultBranchValue of [null, DEFAULT_VALUE]) {
for (const userBranchValue of [null, USER_VALUE]) {
for (const isRollout of [true, false]) {
setPrefs(pref, { defaultBranchValue, userBranchValue });
const doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig(
config,
{ manager, isRollout }
);
PrefUtils.setPref(pref, OVERWRITE_VALUE, { branch });
const enrollments = await store.getAll();
Assert.equal(
enrollments.length,
1,
"There should be one enrollment"
);
Assert.ok(enrollments[0].active, "The enrollment should be active");
Assert.equal(
PrefUtils.getPref(pref, { branch }),
OVERWRITE_VALUE,
`The value of ${pref} on the ${branch} branch should be the expected value`
);
if (branch === USER) {
if (defaultBranchValue) {
Assert.equal(
PrefUtils.getPref(pref, { branch: DEFAULT }),
defaultBranchValue,
"The default branch should have the expected value"
);
} else {
Assert.ok(
!Services.prefs.prefHasDefaultValue(pref),
"The default branch should not have a value"
);
}
} else if (userBranchValue) {
Assert.equal(
PrefUtils.getPref(pref, { branch: USER }),
userBranchValue,
"The user branch should have the expected value"
);
} else {
Assert.ok(
!Services.prefs.prefHasUserValue(pref),
"The user branch should not have a value"
);
}
assertNoObservers(manager);
await doEnrollmentCleanup();
assertEmptyStore(store);
Services.prefs.deleteBranch(pref);
}
}
}
}
cleanupFeature();
await cleanupStore(store);
}
});
add_task(async function test_restorePrefs_manifestChanged() {
TelemetryEvents.init();
const LEGACY_FILTER = {
category: "normandy",
method: "unenroll",
object: "nimbus_experiment",
};
const BOGUS_PREF = "nimbus.test-only.bogus";
const REMOVE_FEATURE = "remove-feature";
const REMOVE_PREF_VARIABLE = "remove-pref-variable";
const REMOVE_OTHER_VARIABLE = "remove-other-variable";
const REMOVE_SETPREF = "remove-setpref";
const CHANGE_SETPREF = "change-setpref";
const OPERATIONS = [
REMOVE_FEATURE,
REMOVE_PREF_VARIABLE,
REMOVE_OTHER_VARIABLE,
REMOVE_SETPREF,
CHANGE_SETPREF,
];
const REASONS = {
[REMOVE_FEATURE]: "invalid-feature",
[REMOVE_PREF_VARIABLE]: "pref-variable-missing",
[REMOVE_SETPREF]: "pref-variable-no-longer",
[CHANGE_SETPREF]: "pref-variable-changed",
};
const featureId = "test-set-pref-temp";
const pref = "nimbus.test-only.baz";
// Return a new object so we can modified the returned value.
function featureFactory(isEarlyStartup) {
return new ExperimentFeature(featureId, {
description: "Test feature that sets a pref on the default branch.",
owner: "test@test.test",
hasExposure: false,
isEarlyStartup,
variables: {
baz: {
type: "string",
description: "Test variable",
setPref: pref,
},
qux: {
type: "string",
description: "Test variable",
},
},
});
}
/*
* Test that enrollments end when the manifest is sufficiently changed and
* that the appropriate telemetry is submitted.
*
* This test sets up some enrollments and saves them to disk. Then the
* manifest will be modified according to `operation`.
*
* A browser restart will be simulated by creating a new ExperimentStore and
* ExperimentManager to restore the saved enrollments.
*
* @param {object} options
*
* @param {string} options.branch
* The name of the pref branch ("user" or "default").
*
* @param {string?} options.defaultBranchValue
* An optional value to set for the pref on the default branch
* before the first enrollment.
*
* @param {string?} options.userBranchValue
* An optional value to set for the pref on the user branch
* before the first enrollment.
*
* @param {object} options.configs
* The rollout and experiment feature configurations.
*
* @param {string} options.operation
* The operation that will be performed on the manifest.
*
* See `OPERATIONS` above.
*
* @param {string[]} options.expectedEnrollments
* The list of enrollment kinds (e.g., "rollout" or "experiment") that
* should be active after setting the pref on the requested branch.
*
* @param {string} options.expectedDefault
* The expected value of the default branch after restoring enrollments.
*
* A value of null indicates that the pref should not be set on the
* default branch.
*
* @param {string} options.expectedUser
* The expected value of the user branch after restoring enrollments.
*
* A value of null indicates that the pref should not be set on the
* user branch.
*/
async function doBaseTest({
branch,
defaultBranchValue = null,
userBranchValue = null,
configs,
operation,
expectedEnrollments = [],
expectedDefault = null,
expectedUser = null,
}) {
Services.fog.testResetFOG();
Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
/* clear = */ true
);
const feature = featureFactory(branch === USER);
const cleanupFeatures = ExperimentTestUtils.addTestFeatures(feature);
setPrefs(pref, { defaultBranchValue, userBranchValue });
const slugs = {};
let userPref = null;
// Enroll in some experiments and save the state to disk.
{
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
assertEmptyStore(store);
for (const [enrollmentKind, config] of Object.entries(configs)) {
const isRollout = enrollmentKind === ROLLOUT;
await ExperimentFakes.enrollWithFeatureConfig(config, {
manager,
isRollout,
});
const enrollments = isRollout
? store.getAllActiveRollouts()
: store.getAllActiveExperiments();
Assert.equal(
enrollments.length,
1,
`Expected one ${enrollmentKind} enrollment`
);
slugs[enrollmentKind] = enrollments[0].slug;
}
store._store.saveSoon();
await store._store.finalize();
// User branch prefs persist through restart, so we only want to delete
// the prefs if we changed the default branch.
if (branch === "user") {
userPref = PrefUtils.getPref(pref, { branch });
}
Services.prefs.deleteBranch(pref);
removePrefObservers(manager);
assertNoObservers(manager);
}
// Restore the default branch value as it was before "restarting".
setPrefs(pref, {
defaultBranchValue,
userBranchValue: userPref ?? userBranchValue,
});
// Mangle the manifest.
switch (operation) {
case REMOVE_FEATURE:
cleanupFeatures();
break;
case REMOVE_PREF_VARIABLE:
delete NimbusFeatures[featureId].manifest.variables.baz;
break;
case REMOVE_OTHER_VARIABLE:
delete NimbusFeatures[featureId].manifest.variables.qux;
break;
case REMOVE_SETPREF:
delete NimbusFeatures[featureId].manifest.variables.baz.setPref;
break;
case CHANGE_SETPREF:
NimbusFeatures[featureId].manifest.variables.baz.setPref = BOGUS_PREF;
break;
default:
Assert.ok(false, "invalid operation");
}
const store = ExperimentFakes.store();
const manager = ExperimentFakes.manager(store);
await manager.onStartup();
for (const enrollmentKind of expectedEnrollments) {
const enrollment = store.get(slugs[enrollmentKind]);
Assert.ok(
enrollment !== null,
`An experiment of kind ${enrollmentKind} should exist`
);
Assert.ok(enrollment.active, "It should still be active");
}
if (expectedDefault === null) {
Assert.ok(
!Services.prefs.prefHasDefaultValue(pref),
`Expected the default branch not to be set for ${pref} value: ${PrefUtils.getPref(
pref,
{ branch: "default" }
)}`
);
} else {
Assert.equal(
Services.prefs.getDefaultBranch(null).getStringPref(pref),
expectedDefault,
`Expected the value of ${pref} on the default branch to match the expected value`
);
}
if (expectedUser === null) {
Assert.ok(
!Services.prefs.prefHasUserValue(pref),
`Expected the user branch not to be set for ${pref} value: ${PrefUtils.getPref(
pref,
{ branch: "user" }
)}`
);
} else {
Assert.equal(
Services.prefs.getStringPref(pref),
expectedUser,
`Expected the value of ${pref} on the user branch to match the expected value`
);
}
if (operation === CHANGE_SETPREF) {
Assert.ok(
!Services.prefs.prefHasDefaultValue(BOGUS_PREF),
"The new pref should not have a value on the default branch"
);
Assert.ok(
!Services.prefs.prefHasUserValue(BOGUS_PREF),
"The new pref should not have a value on the user branch"
);
}
for (const enrollmentKind of Object.keys(configs)) {
if (!expectedEnrollments.includes(enrollmentKind)) {
const slug = slugs[enrollmentKind];
const enrollment = store.get(slug);
Assert.ok(
enrollment !== null,
`An enrollment of kind ${enrollmentKind} should exist`
);
Assert.ok(!enrollment.active, "It should not be active");
store._deleteForTests(slug);
}
}
const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue();
if (expectedEnrollments.length === 0) {
const expectedEvents = [EXPERIMENT, ROLLOUT]
.filter(enrollmentKind => Object.hasOwn(slugs, enrollmentKind))
.map(enrollmentKind => ({
reason: REASONS[operation],
experiment: slugs[enrollmentKind],
}));
// Extract only the values we care about.
const processedEvents = gleanEvents.map(event => ({
reason: event.extra.reason,
experiment: event.extra.experiment,
}));
Assert.deepEqual(
processedEvents,
expectedEvents,
"Glean should have the expected unenrollment events"
);
const expectedLegacyEvents = expectedEvents.map(extra => ({
value: extra.experiment,
extra: pick(extra, "reason"),
}));
TelemetryTestUtils.assertEvents(expectedLegacyEvents, LEGACY_FILTER);
} else {
Assert.equal(
gleanEvents,
undefined,
"Glean should have no unenrollment events"
);
TelemetryTestUtils.assertEvents([], LEGACY_FILTER);
}
for (const enrollmentKind of expectedEnrollments) {
const slug = slugs[enrollmentKind];
manager.unenroll(slug);
store._deleteForTests(slug);
}
assertEmptyStore(store);
await cleanupStore(store);
assertNoObservers(manager);
Services.prefs.deleteBranch(pref);
if (operation !== REMOVE_FEATURE) {
// If we try to remove the feature twice, we will throw an exception.
cleanupFeatures();
}
}
// Test only qux set. These tests should not cause any unenrollments.
{
const quxConfigs = {
[EXPERIMENT]: {
featureId,
value: {
qux: EXPERIMENT_VALUE,
},
},
[ROLLOUT]: {
featureId,
value: {
qux: ROLLOUT_VALUE,
},
},
};
const doTest = ({
branch,
defaultBranchValue = null,
userBranchValue = null,
configs,
operation,
}) =>
doBaseTest({
branch,
configs,
defaultBranchValue,
userBranchValue,
operation,
expectedEnrollments: Object.keys(configs),
expectedDefault: defaultBranchValue,
expectedUser: userBranchValue,
});
for (const branch of [USER, DEFAULT]) {
for (const defaultBranchValue of [null, DEFAULT_VALUE]) {
for (const userBranchValue of [null, USER_VALUE]) {
for (const specifiedConfigs of [
pick(quxConfigs, ROLLOUT),
pick(quxConfigs, EXPERIMENT),
quxConfigs,
]) {
for (const operation of OPERATIONS) {
await doTest({
branch,
defaultBranchValue,
userBranchValue,
configs: specifiedConfigs,
operation,
});
}
}
}
}
}
}
// Test only baz set. All operations except REMOVE_OTHER_VARIABLE will trigger
// unenrollment.
{
const bazConfigs = {
[EXPERIMENT]: {
featureId,
value: {
baz: EXPERIMENT_VALUE,
},
},
[ROLLOUT]: {
featureId,
value: {
baz: ROLLOUT_VALUE,
},
},
};
const doTest = ({
branch,
defaultBranchValue = null,
userBranchValue = null,
configs,
operation,
}) => {
const expectedEnrollments =
operation === REMOVE_OTHER_VARIABLE ? Object.keys(configs) : [];
function expectedPref(forBranch, originalValue) {
if (forBranch === branch) {
if (expectedEnrollments.includes(EXPERIMENT)) {
return EXPERIMENT_VALUE;
} else if (expectedEnrollments.includes(ROLLOUT)) {
return ROLLOUT_VALUE;
}
}
return originalValue;
}
const expectedDefault = expectedPref(DEFAULT, defaultBranchValue);
const expectedUser = expectedPref(USER, userBranchValue);
return doBaseTest({
branch,
configs,
defaultBranchValue,
userBranchValue,
operation,
expectedEnrollments,
expectedDefault,
expectedUser,
});
};
for (const branch of [USER, DEFAULT]) {
for (const defaultBranchValue of [null, DEFAULT_VALUE]) {
for (const userBranchValue of [null, USER_VALUE]) {
for (const specifiedConfigs of [
pick(bazConfigs, ROLLOUT),
pick(bazConfigs, EXPERIMENT),
bazConfigs,
]) {
for (const operation of OPERATIONS) {
await doTest({
branch,
defaultBranchValue,
userBranchValue,
configs: specifiedConfigs,
operation,
});
}
}
}
}
}
}
Services.fog.testResetFOG();
Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
/* clear = */ true
);
});