зеркало из https://github.com/mozilla/gecko-dev.git
Backed out 2 changesets (bug 1594035) for Browser-chrome failiures on normandy/test/browser/browser_Normandy.js. CLOSED TREE
Backed out changeset 066674b8313d (bug 1594035) Backed out changeset 6f93019be0d6 (bug 1594035) --HG-- extra : rebase_source : 7815196d6ce40c374d2ea1d0438230f0bc7201bc
This commit is contained in:
Родитель
98edc028b6
Коммит
f22af0f64c
|
@ -20,7 +20,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
RecipeRunner: "resource://normandy/lib/RecipeRunner.jsm",
|
||||
ShieldPreferences: "resource://normandy/lib/ShieldPreferences.jsm",
|
||||
TelemetryEvents: "resource://normandy/lib/TelemetryEvents.jsm",
|
||||
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
|
||||
});
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Normandy"];
|
||||
|
@ -44,13 +43,6 @@ var Normandy = {
|
|||
rolloutPrefsChanged: {},
|
||||
|
||||
async init({ runAsync = true } = {}) {
|
||||
this.uiAvailableDeferred = PromiseUtils.defer();
|
||||
if (runAsync) {
|
||||
Services.obs.addObserver(this, UI_AVAILABLE_NOTIFICATION);
|
||||
} else {
|
||||
this.uiAvailableDeferred.resolve();
|
||||
}
|
||||
|
||||
// Initialization that needs to happen before the first paint on startup.
|
||||
await NormandyMigrations.applyAll();
|
||||
this.rolloutPrefsChanged = this.applyStartupPrefs(
|
||||
|
@ -60,24 +52,26 @@ var Normandy = {
|
|||
STARTUP_EXPERIMENT_PREFS_BRANCH
|
||||
);
|
||||
|
||||
// Once the UI has loaded (and so we are no longer at risk of delaying it),
|
||||
// call finishInit.
|
||||
this.uiAvailableDeferred.promise.then(() => this.finishInit());
|
||||
if (runAsync) {
|
||||
Services.obs.addObserver(this, UI_AVAILABLE_NOTIFICATION);
|
||||
} else {
|
||||
// Remove any observers, if present.
|
||||
try {
|
||||
Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION);
|
||||
} catch (e) {}
|
||||
|
||||
await this.finishInit();
|
||||
}
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (topic === UI_AVAILABLE_NOTIFICATION) {
|
||||
Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION);
|
||||
this.uiAvailableDeferred.resolve();
|
||||
this.finishInit();
|
||||
}
|
||||
},
|
||||
|
||||
async finishInit() {
|
||||
// Remove any observers, if present.
|
||||
try {
|
||||
Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION);
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
TelemetryEvents.init();
|
||||
} catch (err) {
|
||||
|
|
|
@ -21,16 +21,6 @@ ChromeUtils.defineModuleGetter(
|
|||
"RecipeRunner",
|
||||
"resource://normandy/lib/RecipeRunner.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"AddonRollouts",
|
||||
"resource://normandy/lib/AddonRollouts.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PreferenceRollouts",
|
||||
"resource://normandy/lib/PreferenceRollouts.jsm"
|
||||
);
|
||||
|
||||
var EXPORTED_SYMBOLS = ["NormandyMigrations"];
|
||||
|
||||
|
@ -62,29 +52,20 @@ const NormandyMigrations = {
|
|||
},
|
||||
|
||||
async applyOne(id) {
|
||||
const migration = this.migrations[id]();
|
||||
log.debug(`Running Normandy migration #${id} - ${migration.name}`);
|
||||
const migration = this.migrations[id];
|
||||
log.debug(`Running Normandy migration ${migration.name}`);
|
||||
await migration();
|
||||
},
|
||||
|
||||
migrations: [
|
||||
// Each of these are functions that return the migration function. This way
|
||||
// the lazy importers won't be triggered until we actually need to run the
|
||||
// migration. This saves startup time in the common case that all migrations
|
||||
// are already run.
|
||||
() => migrateShieldPrefs,
|
||||
() => migrateStudiesEnabledWithoutHealthReporting,
|
||||
() =>
|
||||
AddonStudies.migrations.migration01StudyFieldsToSlugAndUserFacingFields,
|
||||
() => PreferenceExperiments.migrations.migration01MoveExperiments,
|
||||
() => PreferenceExperiments.migrations.migration02MultiPreference,
|
||||
() => PreferenceExperiments.migrations.migration03AddActionName,
|
||||
() => PreferenceExperiments.migrations.migration04RenameNameToSlug,
|
||||
() => RecipeRunner.migrations.migration01RemoveOldRecipesCollection,
|
||||
() => PreferenceExperiments.migrations.migration05AddFillerEnrollmentId,
|
||||
() => AddonStudies.migrations.migration02AddFillerEnrollmentId,
|
||||
() => AddonRollouts.migrations.migration01AddFillerEnrollmentId,
|
||||
() => PreferenceRollouts.migrations.migration01AddFillerEnrollmentId,
|
||||
migrateShieldPrefs,
|
||||
migrateStudiesEnabledWithoutHealthReporting,
|
||||
AddonStudies.migrateAddonStudyFieldsToSlugAndUserFacingFields,
|
||||
PreferenceExperiments.migrations.migration01MoveExperiments,
|
||||
PreferenceExperiments.migrations.migration02MultiPreference,
|
||||
PreferenceExperiments.migrations.migration03AddActionName,
|
||||
PreferenceExperiments.migrations.migration04RenameNameToSlug,
|
||||
RecipeRunner.migrations.migration01RemoveOldRecipesCollection,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -14,11 +14,6 @@ ChromeUtils.defineModuleGetter(
|
|||
"TelemetryEnvironment",
|
||||
"resource://gre/modules/TelemetryEnvironment.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"TelemetryEvents",
|
||||
"resource://normandy/lib/TelemetryEvents.jsm"
|
||||
);
|
||||
|
||||
/**
|
||||
* AddonRollouts store info about an active or expired addon rollouts.
|
||||
|
@ -128,40 +123,6 @@ const AddonRollouts = {
|
|||
return getStore(db, "readwrite").put(rollout);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update many existing rollouts. More efficient than calling `update` many
|
||||
* times in a row.
|
||||
* @param {Array<PreferenceRollout>} rollouts
|
||||
* @throws If any of the passed rollouts have a slug that doesn't exist in the database already.
|
||||
*/
|
||||
async updateMany(rollouts) {
|
||||
// Don't touch the database if there is nothing to do
|
||||
if (!rollouts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both of the below operations use .map() instead of a normal loop becaues
|
||||
// once we get the object store, we can't let it expire by spinning the
|
||||
// event loop. This approach queues up all the interactions with the store
|
||||
// immediately, preventing it from expiring too soon.
|
||||
|
||||
const db = await getDatabase();
|
||||
let store = await getStore(db, "readonly");
|
||||
await Promise.all(
|
||||
rollouts.map(async ({ slug }) => {
|
||||
let existingRollout = await store.get(slug);
|
||||
if (!existingRollout) {
|
||||
throw new Error(`Tried to update ${slug}, but it doesn't exist.`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// awaiting spun the event loop, so the store is now invalid. Get a new
|
||||
// store. This is also a chance to get it in readwrite mode.
|
||||
store = await getStore(db, "readwrite");
|
||||
await Promise.all(rollouts.map(rollout => store.put(rollout)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Test whether there is a rollout in storage with the given slug.
|
||||
* @param {string} slug
|
||||
|
@ -213,18 +174,4 @@ const AddonRollouts = {
|
|||
}
|
||||
};
|
||||
},
|
||||
|
||||
migrations: {
|
||||
async migration01AddFillerEnrollmentId() {
|
||||
const rollouts = await AddonRollouts.getAll();
|
||||
const rolloutsToUpdate = [];
|
||||
for (const rollout of rollouts) {
|
||||
if (typeof rollout.enrollmentId != "string") {
|
||||
rollout.enrollmentId = TelemetryEvents.NO_ENROLLMENT_ID_MARKER;
|
||||
rolloutsToUpdate.push(rollout);
|
||||
}
|
||||
}
|
||||
await AddonRollouts.updateMany(rolloutsToUpdate);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -179,6 +179,54 @@ var AddonStudies = {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Change from "name" and "description" to "slug", "userFacingName",
|
||||
* and "userFacingDescription".
|
||||
*
|
||||
* This is called as needed by NormandyMigrations.jsm, which handles tracking
|
||||
* if this migration has already been run.
|
||||
*/
|
||||
async migrateAddonStudyFieldsToSlugAndUserFacingFields() {
|
||||
const db = await getDatabase();
|
||||
const studies = await db.objectStore(STORE_NAME, "readonly").getAll();
|
||||
|
||||
// If there are no studies, stop here to avoid opening the DB again.
|
||||
if (studies.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Object stores expire after `await`, so this method accumulates a bunch of
|
||||
// promises, and then awaits them at the end.
|
||||
const writePromises = [];
|
||||
const objectStore = db.objectStore(STORE_NAME, "readwrite");
|
||||
|
||||
for (const study of studies) {
|
||||
// use existing name as slug
|
||||
if (!study.slug) {
|
||||
study.slug = study.name;
|
||||
}
|
||||
|
||||
// Rename `name` and `description` as `userFacingName` and `userFacingDescription`
|
||||
if (study.name && !study.userFacingName) {
|
||||
study.userFacingName = study.name;
|
||||
}
|
||||
delete study.name;
|
||||
if (study.description && !study.userFacingDescription) {
|
||||
study.userFacingDescription = study.description;
|
||||
}
|
||||
delete study.description;
|
||||
|
||||
// Specify that existing recipes don't have branches
|
||||
if (!study.branch) {
|
||||
study.branch = AddonStudies.NO_BRANCHES_MARKER;
|
||||
}
|
||||
|
||||
writePromises.push(objectStore.put(study));
|
||||
}
|
||||
|
||||
await Promise.all(writePromises);
|
||||
},
|
||||
|
||||
/**
|
||||
* If a study add-on is uninstalled, mark the study as having ended.
|
||||
* @param {Addon} addon
|
||||
|
@ -272,42 +320,6 @@ var AddonStudies = {
|
|||
return getStore(db, "readwrite").put(study);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update many existing studies. More efficient than calling `update` many
|
||||
* times in a row.
|
||||
* @param {Array<AddonStudy>} studies
|
||||
* @throws If any of the passed studies have a slug that doesn't exist in the database already.
|
||||
*/
|
||||
async updateMany(studies) {
|
||||
// Don't touch the database if there is nothing to do
|
||||
if (!studies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both of the below operations use .map() instead of a normal loop becaues
|
||||
// once we get the object store, we can't let it expire by spinning the
|
||||
// event loop. This approach queues up all the interactions with the store
|
||||
// immediately, preventing it from expiring too soon.
|
||||
|
||||
const db = await getDatabase();
|
||||
let store = await getStore(db, "readonly");
|
||||
await Promise.all(
|
||||
studies.map(async ({ recipeId }) => {
|
||||
let existingStudy = await store.get(recipeId);
|
||||
if (!existingStudy) {
|
||||
throw new Error(
|
||||
`Tried to update addon study ${recipeId}, but it doesn't exist.`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// awaiting spun the event loop, so the store is now invalid. Get a new
|
||||
// store. This is also a chance to get it in readwrite mode.
|
||||
store = await getStore(db, "readwrite");
|
||||
await Promise.all(studies.map(study => store.put(study)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a study from storage
|
||||
* @param recipeId The recipeId of the study to delete
|
||||
|
@ -407,56 +419,6 @@ var AddonStudies = {
|
|||
// the listeners fail.
|
||||
await Promise.all(promises);
|
||||
},
|
||||
|
||||
migrations: {
|
||||
/**
|
||||
* Change from "name" and "description" to "slug", "userFacingName",
|
||||
* and "userFacingDescription".
|
||||
*
|
||||
* This is called as needed by NormandyMigrations.jsm, which handles tracking
|
||||
* if this migration has already been run.
|
||||
*/
|
||||
async migration01StudyFieldsToSlugAndUserFacingFields() {
|
||||
// If there are no studies, stop here to avoid opening the DB again.
|
||||
const studies = await AddonStudies.getAll();
|
||||
|
||||
for (const study of studies) {
|
||||
// use existing name as slug
|
||||
if (!study.slug) {
|
||||
study.slug = study.name;
|
||||
}
|
||||
|
||||
// Rename `name` and `description` as `userFacingName` and `userFacingDescription`
|
||||
if (study.name && !study.userFacingName) {
|
||||
study.userFacingName = study.name;
|
||||
}
|
||||
delete study.name;
|
||||
if (study.description && !study.userFacingDescription) {
|
||||
study.userFacingDescription = study.description;
|
||||
}
|
||||
delete study.description;
|
||||
|
||||
// Specify that existing recipes don't have branches
|
||||
if (!study.branch) {
|
||||
study.branch = AddonStudies.NO_BRANCHES_MARKER;
|
||||
}
|
||||
}
|
||||
|
||||
await AddonStudies.updateMany(studies);
|
||||
},
|
||||
|
||||
async migration02AddFillerEnrollmentId() {
|
||||
const studies = await AddonStudies.getAll();
|
||||
const studiesToUpdate = [];
|
||||
for (const study of studies) {
|
||||
if (typeof study.enrollmentId != "string") {
|
||||
study.enrollmentId = TelemetryEvents.NO_ENROLLMENT_ID_MARKER;
|
||||
studiesToUpdate.push(study);
|
||||
}
|
||||
}
|
||||
await AddonStudies.updateMany(studiesToUpdate);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
AddonStudies.NO_BRANCHES_MARKER = "__NO_BRANCHES__";
|
||||
|
|
|
@ -892,13 +892,6 @@ var PreferenceExperiments = {
|
|||
},
|
||||
experimentType: oldExperiment.experimentType,
|
||||
};
|
||||
// Propogate any existing enrollmentId. This isn't likely to ever occur
|
||||
// in the real world, but the tests have mock data that includes an
|
||||
// enrollment ID here, which makes the tests that actually rely on
|
||||
// enrollmentId easier.
|
||||
if (oldExperiment.enrollmentId) {
|
||||
v2Experiments[expName].enrollmentId = oldExperiment.enrollmentId;
|
||||
}
|
||||
}
|
||||
storage.data.experiments = v2Experiments;
|
||||
storage.saveSoon();
|
||||
|
@ -933,18 +926,5 @@ var PreferenceExperiments = {
|
|||
}
|
||||
storage.saveSoon();
|
||||
},
|
||||
|
||||
async migration05AddFillerEnrollmentId(storage = null) {
|
||||
if (!storage) {
|
||||
storage = await ensureStorage();
|
||||
}
|
||||
// Make sure that all experiments have a string enrollmentId
|
||||
for (const experiment of Object.values(storage.data.experiments)) {
|
||||
if (typeof experiment.enrollmentId != "string") {
|
||||
experiment.enrollmentId = TelemetryEvents.NO_ENROLLMENT_ID_MARKER;
|
||||
}
|
||||
}
|
||||
storage.saveSoon();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -228,40 +228,6 @@ var PreferenceRollouts = {
|
|||
return getStore(db, "readwrite").put(rollout);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update many existing rollouts. More efficient than calling `update` many
|
||||
* times in a row.
|
||||
* @param {Array<PreferenceRollout>} rollouts
|
||||
* @throws If any of the passed rollouts have a slug that doesn't exist in the database already.
|
||||
*/
|
||||
async updateMany(rollouts) {
|
||||
// Don't touch the database if there is nothing to do
|
||||
if (!rollouts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both of the below operations use .map() instead of a normal loop becaues
|
||||
// once we get the object store, we can't let it expire by spinning the
|
||||
// event loop. This approach queues up all the interactions with the store
|
||||
// immediately, preventing it from expiring too soon.
|
||||
|
||||
const db = await getDatabase();
|
||||
let store = await getStore(db, "readonly");
|
||||
await Promise.all(
|
||||
rollouts.map(async ({ slug }) => {
|
||||
let existingRollout = await store.get(slug);
|
||||
if (!existingRollout) {
|
||||
throw new Error(`Tried to update ${slug}, but it doesn't exist.`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// awaiting spun the event loop, so the store is now invalid. Get a new
|
||||
// store. This is also a chance to get it in readwrite mode.
|
||||
store = await getStore(db, "readwrite");
|
||||
await Promise.all(rollouts.map(rollout => store.put(rollout)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Test whether there is a rollout in storage with the given slug.
|
||||
* @param {string} slug
|
||||
|
@ -314,18 +280,4 @@ var PreferenceRollouts = {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
migrations: {
|
||||
async migration01AddFillerEnrollmentId() {
|
||||
let rollouts = await PreferenceRollouts.getAll();
|
||||
let rolloutsToUpdate = [];
|
||||
for (const rollout of rollouts) {
|
||||
if (typeof rollout.enrollmentId != "string") {
|
||||
rollout.enrollmentId = TelemetryEvents.NO_ENROLLMENT_ID_MARKER;
|
||||
rolloutsToUpdate.push(rollout);
|
||||
}
|
||||
}
|
||||
await PreferenceRollouts.updateMany(rolloutsToUpdate);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,8 +10,6 @@ var EXPORTED_SYMBOLS = ["TelemetryEvents"];
|
|||
const TELEMETRY_CATEGORY = "normandy";
|
||||
|
||||
const TelemetryEvents = {
|
||||
NO_ENROLLMENT_ID_MARKER: "__NO_ENROLLMENT_ID__",
|
||||
|
||||
init() {
|
||||
Services.telemetry.setEventRecordingEnabled(TELEMETRY_CATEGORY, true);
|
||||
},
|
||||
|
|
|
@ -48,14 +48,35 @@ function experimentFactory(attrs) {
|
|||
|
||||
const NOW = new Date();
|
||||
|
||||
// Note that the below data is not entirely representative of what we see in the
|
||||
// real world. Specifically, even the older format includes one experiment with
|
||||
// an enrollmentId. There isn't any way this could have happened in the real
|
||||
// world. However, including it here lets us use a consistent set of data for
|
||||
// all the tests. The simplification of the tests seems worth it.
|
||||
const mockDataVersions = [
|
||||
// before any migrations
|
||||
{
|
||||
const mockV1Data = {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferenceName: "some.pref",
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
experimentType: "exp",
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
branch: "another_4",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferenceName: "another.pref",
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
experimentType: "exp",
|
||||
},
|
||||
};
|
||||
|
||||
const mockV2Data = {
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
|
@ -67,7 +88,6 @@ const mockDataVersions = [
|
|||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
experimentType: "exp",
|
||||
enrollmentId: "existing-enrollment-id",
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
|
@ -82,193 +102,116 @@ const mockDataVersions = [
|
|||
experimentType: "exp",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// after migration 01
|
||||
{
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferenceName: "some.pref",
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
experimentType: "exp",
|
||||
enrollmentId: "existing-enrollment-id",
|
||||
const mockV3Data = {
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
branch: "another_4",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferenceName: "another.pref",
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
experimentType: "exp",
|
||||
experimentType: "exp",
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
branch: "another_4",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// after migration 02
|
||||
{
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
const mockV4Data = {
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
experimentType: "exp",
|
||||
enrollmentId: "existing-enrollment-id",
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
branch: "another_4",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
branch: "another_4",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// after migration 03
|
||||
{
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
name: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
const mockV5Data = {
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
slug: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
experimentType: "exp",
|
||||
enrollmentId: "existing-enrollment-id",
|
||||
},
|
||||
another_experiment: {
|
||||
name: "another_experiment",
|
||||
branch: "another_4",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
another_experiment: {
|
||||
slug: "another_experiment",
|
||||
branch: "another_4",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
},
|
||||
|
||||
// after migration 04
|
||||
{
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
slug: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
},
|
||||
experimentType: "exp",
|
||||
enrollmentId: "existing-enrollment-id",
|
||||
},
|
||||
another_experiment: {
|
||||
slug: "another_experiment",
|
||||
branch: "another_4",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
},
|
||||
experimentType: "exp",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// after migration 05
|
||||
{
|
||||
experiments: {
|
||||
hypothetical_experiment: {
|
||||
slug: "hypothetical_experiment",
|
||||
branch: "hypo_1",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: false,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"some.pref": {
|
||||
preferenceValue: 2,
|
||||
preferenceType: "integer",
|
||||
previousPreferenceValue: 1,
|
||||
preferenceBranchType: "user",
|
||||
},
|
||||
},
|
||||
experimentType: "exp",
|
||||
enrollmentId: "existing-enrollment-id",
|
||||
},
|
||||
another_experiment: {
|
||||
slug: "another_experiment",
|
||||
branch: "another_4",
|
||||
actionName: "SinglePreferenceExperimentAction",
|
||||
expired: true,
|
||||
lastSeen: NOW.toJSON(),
|
||||
preferences: {
|
||||
"another.pref": {
|
||||
preferenceValue: true,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: false,
|
||||
preferenceBranchType: "default",
|
||||
},
|
||||
},
|
||||
experimentType: "exp",
|
||||
enrollmentId: "__NO_ENROLLMENT_ID__",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a mock `JsonFile` object with a no-op `saveSoon` method and a deep copy
|
||||
|
@ -283,33 +226,37 @@ function makeMockJsonFile(data = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
let migrationsList = [
|
||||
PreferenceExperiments.migrations.migration01MoveExperiments,
|
||||
PreferenceExperiments.migrations.migration02MultiPreference,
|
||||
PreferenceExperiments.migrations.migration03AddActionName,
|
||||
PreferenceExperiments.migrations.migration04RenameNameToSlug,
|
||||
PreferenceExperiments.migrations.migration05AddFillerEnrollmentId,
|
||||
];
|
||||
|
||||
/** Test that each migration results in the expected data */
|
||||
add_task(async function test_migrations() {
|
||||
let mockJsonFile = makeMockJsonFile(mockDataVersions[0]);
|
||||
for (let i = 0; i < migrationsList.length; i++) {
|
||||
await migrationsList[i](mockJsonFile);
|
||||
Assert.deepEqual(
|
||||
mockJsonFile.data,
|
||||
mockDataVersions[i + 1],
|
||||
`Migration ${i + 1} should produce the expected results`
|
||||
);
|
||||
}
|
||||
let mockJsonFile = makeMockJsonFile(mockV1Data);
|
||||
await PreferenceExperiments.migrations.migration01MoveExperiments(
|
||||
mockJsonFile
|
||||
);
|
||||
Assert.deepEqual(mockJsonFile.data, mockV2Data);
|
||||
|
||||
mockJsonFile = makeMockJsonFile(mockV2Data);
|
||||
await PreferenceExperiments.migrations.migration02MultiPreference(
|
||||
mockJsonFile
|
||||
);
|
||||
Assert.deepEqual(mockJsonFile.data, mockV3Data);
|
||||
|
||||
mockJsonFile = makeMockJsonFile(mockV3Data);
|
||||
await PreferenceExperiments.migrations.migration03AddActionName(mockJsonFile);
|
||||
Assert.deepEqual(mockJsonFile.data, mockV4Data);
|
||||
|
||||
mockJsonFile = makeMockJsonFile(mockV4Data);
|
||||
await PreferenceExperiments.migrations.migration04RenameNameToSlug(
|
||||
mockJsonFile
|
||||
);
|
||||
Assert.deepEqual(mockJsonFile.data, mockV5Data);
|
||||
});
|
||||
|
||||
add_task(async function migration03KeepsActionName() {
|
||||
let mockData = JSON.parse(JSON.stringify(mockDataVersions[2]));
|
||||
let mockData = JSON.parse(JSON.stringify(mockV3Data));
|
||||
mockData.experiments.another_experiment.actionName = "SomeOldAction";
|
||||
const mockJsonFile = makeMockJsonFile(mockData);
|
||||
// Output should be the same as mockDataVersions[3], but preserving the action.
|
||||
const migratedData = JSON.parse(JSON.stringify(mockDataVersions[3]));
|
||||
// Output should be the same as mockV4Data, but preserving the action.
|
||||
const migratedData = JSON.parse(JSON.stringify(mockV4Data));
|
||||
migratedData.experiments.another_experiment.actionName = "SomeOldAction";
|
||||
|
||||
await PreferenceExperiments.migrations.migration03AddActionName(mockJsonFile);
|
||||
|
@ -317,9 +264,13 @@ add_task(async function migration03KeepsActionName() {
|
|||
});
|
||||
|
||||
add_task(async function migrations_are_idempotent() {
|
||||
for (let i = 0; i < migrationsList.length; i++) {
|
||||
const migration = migrationsList[i];
|
||||
const mockOldData = mockDataVersions[i];
|
||||
let dataVersions = [
|
||||
[PreferenceExperiments.migrations.migration01MoveExperiments, mockV1Data],
|
||||
[PreferenceExperiments.migrations.migration02MultiPreference, mockV2Data],
|
||||
[PreferenceExperiments.migrations.migration03AddActionName, mockV3Data],
|
||||
[PreferenceExperiments.migrations.migration04RenameNameToSlug, mockV4Data],
|
||||
];
|
||||
for (const [migration, mockOldData] of dataVersions) {
|
||||
const mockJsonFileOnce = makeMockJsonFile(mockOldData);
|
||||
const mockJsonFileTwice = makeMockJsonFile(mockOldData);
|
||||
await migration(mockJsonFileOnce);
|
||||
|
|
Загрузка…
Ссылка в новой задаче