350 lines
9.1 KiB
JavaScript
350 lines
9.1 KiB
JavaScript
"use strict";
|
|
|
|
const { BaseAction } = ChromeUtils.importESModule(
|
|
"resource://normandy/actions/BaseAction.sys.mjs"
|
|
);
|
|
const { Uptake } = ChromeUtils.importESModule(
|
|
"resource://normandy/lib/Uptake.sys.mjs"
|
|
);
|
|
|
|
class NoopAction extends BaseAction {
|
|
constructor() {
|
|
super();
|
|
this._testPreExecutionFlag = false;
|
|
this._testRunFlag = false;
|
|
this._testFinalizeFlag = false;
|
|
}
|
|
|
|
_preExecution() {
|
|
this._testPreExecutionFlag = true;
|
|
}
|
|
|
|
_run() {
|
|
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() {
|
|
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.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
|
|
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.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
|
|
Assert.deepEqual(
|
|
reportRecipeStub.args,
|
|
[[recipe, 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();
|
|
Assert.equal(
|
|
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.processRecipe(recipe1, BaseAction.suitability.FILTER_MATCH);
|
|
await action.finalize();
|
|
|
|
await Assert.rejects(
|
|
action.processRecipe(recipe2, BaseAction.suitability.FILTER_MATCH),
|
|
/^Error: Action has already been finalized$/,
|
|
"running recipes after finalization is an error"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
reportRecipeStub.args,
|
|
[[recipe1, 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 `processRecipe` should not itself throw an exception.
|
|
await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
|
|
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, 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.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
|
|
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, 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.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
|
|
await action.finalize();
|
|
|
|
Assert.deepEqual(
|
|
reportRecipeStub.args,
|
|
[[recipe, 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.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
|
|
|
|
// 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, Uptake.RECIPE_ACTION_DISABLED]],
|
|
"Recipe should report recipe status as action disabled"
|
|
);
|
|
}
|
|
);
|
|
|
|
// If the capabilities don't match, processRecipe shouldn't validate the arguments
|
|
decorate_task(async function () {
|
|
const recipe = recipeFactory();
|
|
const action = new NoopAction();
|
|
const verifySpy = sinon.spy(action, "validateArguments");
|
|
await action.processRecipe(
|
|
recipe,
|
|
BaseAction.suitability.CAPABILITIES_MISMATCH
|
|
);
|
|
ok(!verifySpy.called, "validateArguments should not be called");
|
|
});
|