gecko-dev/toolkit/components/normandy/test/browser/browser_BaseAction.js
Ethan Glasser-Camp 428ab05ab2 Bug 1440779: BaseAction: move _preExecute call out of constructor r=mythmon,Gijs
Having _preExecute be called in the constructor makes it hard to test
it, but because it's a lifecycle hook that subclasses are intended to
override, testing it is very natural.

While we're here, move the initialization of AddonStudyAction from
_preExecute to the constructor, since it doesn't really make sense for
AddonStudyAction to be constructed without all members initialized.

Differential Revision: https://phabricator.services.mozilla.com/D13572

--HG--
extra : moz-landing-system : lando
2018-12-13 16:02:25 +00:00

250 lines
7.8 KiB
JavaScript

"use strict";
ChromeUtils.import("resource://normandy/actions/BaseAction.jsm", this);
ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
class NoopAction extends BaseAction {
constructor() {
super();
this._testPreExecutionFlag = false;
this._testRunFlag = false;
this._testFinalizeFlag = false;
}
_preExecution() {
this._testPreExecutionFlag = true;
}
_run(recipe) {
this._testRunFlag = true;
}
_finalize() {
this._testFinalizeFlag = true;
}
}
NoopAction._errorToThrow = new Error("test error");
class FailPreExecutionAction extends NoopAction {
_preExecution() {
throw NoopAction._errorToThrow;
}
}
class FailRunAction extends NoopAction {
_run(recipe) {
throw NoopAction._errorToThrow;
}
}
class FailFinalizeAction extends NoopAction {
_finalize() {
throw NoopAction._errorToThrow;
}
}
// Test that constructor and override methods are run
decorate_task(
withStub(Uptake, "reportRecipe"),
withStub(Uptake, "reportAction"),
async () => {
let action = new NoopAction();
is(action._testPreExecutionFlag, false, "_preExecution should not have been called on a new action");
is(action._testRunFlag, false, "_run has should not have been called on a new action");
is(action._testFinalizeFlag, false, "_finalize should not be called on a new action");
const recipe = recipeFactory();
await action.runRecipe(recipe);
is(action._testPreExecutionFlag, true, "_preExecution should be called when a recipe is executed");
is(action._testRunFlag, true, "_run should be called when a recipe is executed");
is(action._testFinalizeFlag, false, "_finalize should not have been called when a recipe is executed");
await action.finalize();
is(action._testFinalizeFlag, true, "_finalizeExecution should be called when finalize was called");
action = new NoopAction();
await action.finalize();
is(action._testPreExecutionFlag, true, "_preExecution should be called when finalized even if no recipes");
is(action._testRunFlag, false, "_run should be called if no recipes were run");
is(action._testFinalizeFlag, true, "_finalize should be called when finalized");
}
);
// Test that per-recipe uptake telemetry is recorded
decorate_task(
withStub(Uptake, "reportRecipe"),
async function(reportRecipeStub) {
const action = new NoopAction();
const recipe = recipeFactory();
await action.runRecipe(recipe);
Assert.deepEqual(
reportRecipeStub.args,
[[recipe.id, Uptake.RECIPE_SUCCESS]],
"per-recipe uptake telemetry should be reported",
);
},
);
// Finalize causes action telemetry to be recorded
decorate_task(
withStub(Uptake, "reportAction"),
async function(reportActionStub) {
const action = new NoopAction();
await action.finalize();
ok(action.state == NoopAction.STATE_FINALIZED, "Action should be marked as finalized");
Assert.deepEqual(
reportActionStub.args,
[[action.name, Uptake.ACTION_SUCCESS]],
"action uptake telemetry should be reported",
);
},
);
// Recipes can't be run after finalize is called
decorate_task(
withStub(Uptake, "reportRecipe"),
async function(reportRecipeStub) {
const action = new NoopAction();
const recipe1 = recipeFactory();
const recipe2 = recipeFactory();
await action.runRecipe(recipe1);
await action.finalize();
await Assert.rejects(
action.runRecipe(recipe2),
/^Error: Action has already been finalized$/,
"running recipes after finalization is an error",
);
Assert.deepEqual(
reportRecipeStub.args,
[[recipe1.id, Uptake.RECIPE_SUCCESS]],
"Only recipes executed prior to finalizer should report uptake telemetry",
);
},
);
// Test an action with a failing pre-execution step
decorate_task(
withStub(Uptake, "reportRecipe"),
withStub(Uptake, "reportAction"),
async function(reportRecipeStub, reportActionStub) {
const recipe = recipeFactory();
const action = new FailPreExecutionAction();
is(action.state, FailPreExecutionAction.STATE_PREPARING, "Pre-execution should not happen immediately");
// Should fail, putting the action into a "failed" state, but the
// entry point `runRecipe()` itself should not throw an exception.
await action.runRecipe(recipe);
is(action.state, FailPreExecutionAction.STATE_FAILED, "Action fails if pre-execution fails");
is(action.lastError, NoopAction._errorToThrow, "The thrown error should be stored in lastError");
// Should not throw, even though the action is in a disabled state.
await action.finalize();
is(action.state, FailPreExecutionAction.STATE_FINALIZED, "Action should be finalized");
is(action.lastError, NoopAction._errorToThrow, "lastError should not have changed");
is(action._testRunFlag, false, "_run should not have been called");
is(action._testFinalizeFlag, false, "_finalize should not have been called");
Assert.deepEqual(
reportRecipeStub.args,
[[recipe.id, Uptake.RECIPE_ACTION_DISABLED]],
"Recipe should report recipe status as action disabled",
);
Assert.deepEqual(
reportActionStub.args,
[[action.name, Uptake.ACTION_PRE_EXECUTION_ERROR]],
"Action should report pre execution error",
);
},
);
// Test an action with a failing recipe step
decorate_task(
withStub(Uptake, "reportRecipe"),
withStub(Uptake, "reportAction"),
async function(reportRecipeStub, reportActionStub) {
const recipe = recipeFactory();
const action = new FailRunAction();
await action.runRecipe(recipe);
is(action.state, FailRunAction.STATE_READY, "Action should not be marked as failed due to a recipe failure");
await action.finalize();
is(action.state, FailRunAction.STATE_FINALIZED, "Action should be marked as finalized after finalize is called");
ok(action._testFinalizeFlag, "_finalize should have been called");
Assert.deepEqual(
reportRecipeStub.args,
[[recipe.id, Uptake.RECIPE_EXECUTION_ERROR]],
"Recipe should report recipe execution error",
);
Assert.deepEqual(
reportActionStub.args,
[[action.name, Uptake.ACTION_SUCCESS]],
"Action should report success",
);
},
);
// Test an action with a failing finalize step
decorate_task(
withStub(Uptake, "reportRecipe"),
withStub(Uptake, "reportAction"),
async function(reportRecipeStub, reportActionStub) {
const recipe = recipeFactory();
const action = new FailFinalizeAction();
await action.runRecipe(recipe);
await action.finalize();
Assert.deepEqual(
reportRecipeStub.args,
[[recipe.id, Uptake.RECIPE_SUCCESS]],
"Recipe should report success",
);
Assert.deepEqual(
reportActionStub.args,
[[action.name, Uptake.ACTION_POST_EXECUTION_ERROR]],
"Action should report post execution error",
);
},
);
// Disable disables an action
decorate_task(
withStub(Uptake, "reportRecipe"),
withStub(Uptake, "reportAction"),
async function(reportRecipeStub, reportActionStub) {
const recipe = recipeFactory();
const action = new NoopAction();
action.disable();
is(action.state, NoopAction.STATE_DISABLED, "Action should be marked as disabled");
// Should not throw, even though the action is disabled
await action.runRecipe(recipe);
// Should not throw, even though the action is disabled
await action.finalize();
is(action._testRunFlag, false, "_run should not have been called");
is(action._testFinalizeFlag, false, "_finalize should not have been called");
Assert.deepEqual(
reportActionStub.args,
[[action.name, Uptake.ACTION_SUCCESS]],
"Action should not report pre execution error",
);
Assert.deepEqual(
reportRecipeStub.args,
[[recipe.id, Uptake.RECIPE_ACTION_DISABLED]],
"Recipe should report recipe status as action disabled",
);
},
);