Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
const { ExperimentAPI, NimbusFeatures } = ChromeUtils.importESModule(
"resource://nimbus/ExperimentAPI.sys.mjs"
);
const {
LABS_MIGRATION_FEATURE_MAP,
MigrationError,
NIMBUS_MIGRATION_PREF,
NimbusMigrations,
} = ChromeUtils.importESModule("resource://nimbus/lib/Migrations.sys.mjs");
function mockLabsRecipes(targeting = "true") {
return Object.entries(LABS_MIGRATION_FEATURE_MAP).map(([featureId, slug]) =>
ExperimentFakes.recipe(slug, {
isRollout: true,
isFirefoxLabsOptIn: true,
firefoxLabsTitle: `${featureId}-placeholder-title`,
firefoxLabsDescription: `${featureId}-placeholder-desc`,
firefoxLabsDescriptionLinks: null,
firefoxLabsGroup: "placeholder",
bucketConfig: {
...ExperimentFakes.recipe.bucketConfig,
count: 1000,
},
branches: [
{
slug: "control",
ratio: 1,
features: [
{
featureId,
value: {
enabled: true,
},
},
],
},
],
targeting,
})
);
}
function getEnabledPrefForFeature(featureId) {
return NimbusFeatures[featureId].manifest.variables.enabled.setPref.pref;
}
function removeExperimentManagerListeners(manager) {
// This is a giant hack to remove pref listeners from the global ExperimentManager or an
// ExperimentManager from a previous test (because the nsIObserverService holds a strong reference
// to all these listeners);
//
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1950237 for a long-term solution to this.
Services.prefs.removeObserver(
"datareporting.healthreport.uploadEnabled",
manager
);
Services.prefs.removeObserver("app.shield.optoutstudies.enabled", manager);
}
add_setup(function setup() {
do_get_profile();
Services.fog.initializeFOG();
removeExperimentManagerListeners(ExperimentAPI._manager);
});
/**
* Setup a test environment.
*
* @param {object} options
* @param {number?} options.latestMigration
* The value that should be set for the latest Nimbus migration
* pref. If not provided, the pref will be unset.
* @param {object[]} options.migrations
* An array of migrations that will replace the regular set of
* migrations for the duration of the test.
* @param {object[]} options.recipes
* An array of experiment recipes that will be returned by the
* RemoteSettingsExperimentLoader for the duration of the test.
* @param {boolean} options.init
* If true, the ExperimentAPI will be initialized during the setup.
*/
async function setupTest({
latestMigration,
migrations,
recipes,
init = true,
} = {}) {
const sandbox = sinon.createSandbox();
const loader = ExperimentFakes.rsLoader();
sandbox.stub(ExperimentAPI, "_rsLoader").get(() => loader);
sandbox.stub(ExperimentAPI, "_manager").get(() => loader.manager);
sandbox.stub(loader, "setTimer");
Assert.ok(
!Services.prefs.prefHasUserValue(NIMBUS_MIGRATION_PREF),
"migration pref should be unset"
);
if (typeof latestMigration !== "undefined") {
Services.prefs.setIntPref(NIMBUS_MIGRATION_PREF, latestMigration);
}
if (Array.isArray(migrations)) {
sandbox.stub(NimbusMigrations, "MIGRATIONS").get(() => migrations);
}
if (Array.isArray(recipes)) {
sandbox
.stub(loader.remoteSettingsClients.experiments, "get")
.resolves(recipes);
}
if (init) {
await ExperimentAPI.init();
await ExperimentAPI.ready();
}
return {
sandbox,
loader,
manager: loader.manager,
cleanup() {
assertEmptyStore(loader.manager.store);
ExperimentAPI._resetForTests();
removeExperimentManagerListeners(loader.manager);
Services.prefs.deleteBranch(NIMBUS_MIGRATION_PREF);
sandbox.restore();
Services.fog.testResetFOG();
},
};
}
function makeMigrations(count) {
const migrations = [];
for (let i = 0; i < count; i++) {
migrations.push(
NimbusMigrations.migration(`test-migration-${i}`, sinon.stub())
);
}
return migrations;
}
add_task(async function test_migration_unset() {
info("Testing NimbusMigrations with no migration pref set");
const migrations = makeMigrations(2);
const { cleanup } = await setupTest({ migrations });
Assert.ok(
migrations[0].fn.calledOnce,
`${migrations[0].name} should be called once`
);
Assert.ok(
migrations[1].fn.calledOnce,
`${migrations[1].name} should be called once`
);
Assert.equal(
Services.prefs.getIntPref(NIMBUS_MIGRATION_PREF),
1,
"Migration pref should be updated"
);
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
success: "true",
migration_id: migrations[0].name,
},
{
success: "true",
migration_id: migrations[1].name,
},
]
);
cleanup();
});
add_task(async function test_migration_partially_done() {
info("Testing NimbusMigrations with some migrations completed");
const migrations = makeMigrations(2);
const { cleanup } = await setupTest({ latestMigration: 0, migrations });
Assert.ok(
migrations[0].fn.notCalled,
`${migrations[0].name} should not be called`
);
Assert.ok(
migrations[1].fn.calledOnce,
`${migrations[1].name} should be called once`
);
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
success: "true",
migration_id: migrations[1].name,
},
]
);
cleanup();
});
add_task(async function test_migration_throws() {
info(
"Testing NimbusMigrations with a migration that throws an unknown error"
);
const migrations = makeMigrations(3);
migrations[1].fn.throws(new Error(`${migrations[1].name} failed`));
const { cleanup } = await setupTest({ migrations });
Assert.ok(
migrations[0].fn.calledOnce,
`${migrations[0].name} should be called once`
);
Assert.ok(
migrations[1].fn.calledOnce,
`${migrations[1].name} should be called once`
);
Assert.ok(
migrations[2].fn.notCalled,
`${migrations[2].name} should not be called`
);
Assert.equal(
Services.prefs.getIntPref(NIMBUS_MIGRATION_PREF),
0,
"Migration pref should only be set to 0"
);
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
success: "true",
migration_id: migrations[0].name,
},
{
success: "false",
migration_id: migrations[1].name,
error_reason: MigrationError.Reason.UNKNOWN,
},
]
);
cleanup();
});
add_task(async function test_migration_throws_MigrationError() {
info(
"Testing NimbusMigrations with a migration that throws a MigrationError"
);
const migrations = makeMigrations(3);
migrations[1].fn.throws(new MigrationError("bogus"));
const { cleanup } = await setupTest({ migrations });
Assert.ok(
migrations[0].fn.calledOnce,
`${migrations[0].name} should be called once`
);
Assert.ok(
migrations[1].fn.calledOnce,
`${migrations[1].name} should be called once`
);
Assert.ok(
migrations[2].fn.notCalled,
`${migrations[2].name} should not be called`
);
Assert.equal(
Services.prefs.getIntPref(NIMBUS_MIGRATION_PREF),
0,
"Migration pref should only be set to 0"
);
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
success: "true",
migration_id: migrations[0].name,
},
{
success: "false",
migration_id: migrations[1].name,
error_reason: "bogus",
},
]
);
cleanup();
});
add_task(async function test_migration_firefoxLabsEnrollments() {
async function doTest(features) {
info(
`Testing NimbusMigrations migrates Firefox Labs features ${JSON.stringify(features)}`
);
const prefs = features.map(getEnabledPrefForFeature);
for (const pref of prefs) {
Services.prefs.setBoolPref(pref, true);
}
const { manager, cleanup } = await setupTest({
recipes: mockLabsRecipes("true"),
});
Assert.deepEqual(
await ExperimentAPI._manager
.getAllOptInRecipes()
.then(recipes => recipes.map(recipe => recipe.slug).toSorted()),
Object.values(LABS_MIGRATION_FEATURE_MAP).toSorted(),
"The labs recipes should be available"
);
for (const [feature, slug] of Object.entries(LABS_MIGRATION_FEATURE_MAP)) {
const enrollmentExpected = features.includes(feature);
const metadata = ExperimentAPI.getRolloutMetaData({ slug });
if (enrollmentExpected) {
Assert.ok(!!metadata, `There should be an enrollment for slug ${slug}`);
const pref = getEnabledPrefForFeature(feature);
Assert.equal(
Services.prefs.getBoolPref(pref),
true,
`Pref ${pref} should be set after enrollment`
);
manager.unenroll(slug);
Assert.equal(
Services.prefs.getBoolPref(pref),
false,
`Pref ${pref} should be unset after unenrollment`
);
Assert.ok(
!Services.prefs.prefHasUserValue(pref),
`Pref ${pref} should not be set on the user branch`
);
} else {
Assert.ok(
!metadata,
`There should not be an enrollment for slug ${slug}`
);
}
}
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
success: "true",
migration_id: "firefox-labs-enrollments",
},
]
);
cleanup();
}
await doTest([]);
for (const feature of Object.keys(LABS_MIGRATION_FEATURE_MAP)) {
await doTest([feature]);
}
await doTest(Object.keys(LABS_MIGRATION_FEATURE_MAP));
});
add_task(async function test_migration_firefoxLabsEnrollments_falseTargeting() {
// Some of the features will be limited to specific channels.
// We don't need to test that targeting evaluation itself works, so we'll just
// test with hardcoded targeting.
info(
`Testing NimbusMigration does not migrate Firefox Labs features when targeting is false`
);
const prefs = Object.keys(LABS_MIGRATION_FEATURE_MAP).map(
getEnabledPrefForFeature
);
for (const pref of prefs) {
Services.prefs.setBoolPref(pref, true);
}
const { manager, cleanup } = await setupTest({
recipes: mockLabsRecipes("false"),
});
Assert.deepEqual(
await ExperimentAPI._manager.getAllOptInRecipes(),
[],
"There should be no opt-in recipes"
);
for (const pref of prefs) {
Assert.ok(
Services.prefs.getBoolPref(pref),
`Pref ${pref} should be unchanged`
);
Services.prefs.clearUserPref(pref);
}
for (const slug of Object.values(LABS_MIGRATION_FEATURE_MAP)) {
Assert.ok(
typeof manager.store.get(slug) === "undefined",
`There should be no store entry for ${slug}`
);
}
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
success: "true",
migration_id: "firefox-labs-enrollments",
},
]
);
cleanup();
});
add_task(async function test_migration_firefoxLabsEnrollments_idempotent() {
info("Testing the firefox-labs-enrollments migration is idempotent");
const prefs = Object.keys(LABS_MIGRATION_FEATURE_MAP).map(
getEnabledPrefForFeature
);
for (const pref of prefs) {
Services.prefs.setBoolPref(pref, true);
}
const recipes = mockLabsRecipes("true");
// Get the store into a partially migrated state (i.e., we have enrolled in at least one
// experiment but the migration pref has not updated).
{
const manager = ExperimentFakes.manager();
await manager.onStartup();
manager.enroll(recipes[0], "rs-loader", { branchSlug: "control" });
await manager.store._store.saveSoon();
await manager.store._store.finalize();
removeExperimentManagerListeners(manager);
}
const { manager, cleanup } = await setupTest({
recipes,
});
Assert.equal(
Services.prefs.getIntPref(NIMBUS_MIGRATION_PREF),
0,
"Migration pref updated"
);
Assert.deepEqual(
Glean.nimbusEvents.migration.testGetValue().map(ev => ev.extra),
[
{
migration_id: "firefox-labs-enrollments",
success: "true",
},
]
);
for (const { slug } of recipes) {
manager.unenroll(slug);
}
cleanup();
for (const pref of prefs) {
Services.prefs.clearUserPref(pref);
}
});