¶ ScenarioA scenario is a series of steps that can make HTTP calls and process data. This class contains mostly the flow control code that executes steps. It also handles emitting events that are used by other components, e.g. for logging. Related components:
|
|
¶ EventsA scenario is an event emitter. The following events can be emitted:
See Run Algorithm and client.js for more information on these events. |
|
¶ DependenciesThe following external libraries are used:
|
var _ = require('underscore'),
events = require('events'),
merge = require('deepmerge'),
q = require('q'),
slice = Array.prototype.slice,
util = require('util');
var LOG_LEVELS = [ 'trace', 'debug', 'info' ];
|
¶ ExportsThis module exports a factory function that can be used to inject mock dependencies. The following dependencies must be passed to the factory function in order:
For example:
|
module.exports = function(extensions, log4js, print) {
|
¶ ConstructorConstructs a new scenario with no steps. Options:
|
function Scenario(options) {
if (!_.isObject(options)) {
throw new Error('Options must be an object');
} else if (!_.isString(options.name)) {
throw new Error('"name" must be a string, got ' + options.name);
}
this.name = options.name;
this.summary = options.summary;
this.logger = log4js.getLogger(this.name);
this.baseOptions = _.extend({}, options);
this.steps = [];
_.each(this.initializers, function(initializer) {
initializer.call(this);
}, this);
|
Most of these options are not handled by the scenario object itself.
The The scenario itself listens to that event, and also forwards it to the HTTP client. |
events.EventEmitter.call(this);
this.on('configure', _.bind(this.onConfigure, this));
}
|
A scenario is an event emitter. |
util.inherits(Scenario, events.EventEmitter);
_.extend(Scenario.prototype, {
initializers: [],
beforeRun: [],
defaultRunOptions: {
log: 'info'
},
|
¶ Flow Control Methods |
|
¶ #step(name, definition)Adds a named step to this scenario. The name is mandatory and must be unique as it can be used for flow control (see The definition must be a function. The value it returns will be the first argument of the next step.
A step can also return a promise. In that case, its future value will be the first argument of the next step when the promise is resolved. For example, the HTTP client returns a promise.
If the promise is rejected, the step fails and the scenario is interrupted. |
step: function(name, definition) {
if (!_.isString(name)) {
throw new Error('Step name must be a string, got ' + typeof(name));
} else if (typeof(definition) != 'function') {
throw new Error('Step definition must be a function, got ' + typeof(definition));
} else if (this.findStep(name)) {
throw new Error('Step "' + name + '" is already defined');
}
this.steps.push({ name: name, definition: definition });
return this;
},
|
¶ #setNextStep(name)By default, steps are executed in order. Call this method with the name of the step you want to go to once the current one has completed.
|
setNextStep: function(name) {
this.nextStep = this.findStep(name);
if (!this.nextStep) {
throw new Error('No such step "' + name + '"');
}
},
|
¶ #success(args...)Returns a resolved promise that can be used to pass multiple results to the next step.
|
success: function() {
return q(new StepArgs(slice.call(arguments)));
},
|
¶ #skip(message, args...)Returns a resolved promise similar to the one returned by The first argument is a message describing the reason for skipping this step. It will not be passed to the next step. Additionally, the step will end with a |
skip: function(message) {
if (message && !_.isString(message)) {
throw new Error('Skip message must be a string, got ' + typeof(message));
}
this.stepSkipped = true;
this.skipMessage = message;
return q(new StepArgs(slice.call(arguments, 1)));
},
|
¶ #fail(errorMessage)Returns a rejected promise that can be used to interrupt the scenario.
|
fail: function(err) {
return q.reject(err);
},
|
¶ #complete()Marks the scenario as completed after the current step is executed. Further steps will not be executed and the scenario will complete successfully. |
complete: function() {
this.completed = true;
},
|
¶ Utility MethodsAlso see scenario.ext.client.js for methods to make HTTP calls. |
|
¶ #defer()Returns a deferred object from the q library. |
defer: function() {
return q.defer();
},
|
¶ #all(promises...)Returns a promise for an array of promises. See q combinations. |
all: function() {
return q.all.apply(q, slice.call(arguments));
},
|
¶ Internals |
|
¶ #configure(options)Emits the |
configure: function(options) {
this.emit('configure', options);
},
|
¶ #onConfigure(options)Called when the |
onConfigure: function(options) {
options = options || {};
this.validateRunOptions(options);
if (_.has(options, 'log') && this.logger.level && this.logger.level.toString() != options.log.toUpperCase()) {
this.logger.setLevel(options.log.toUpperCase());
}
},
|
¶ #findStep(name)Returns the data object for the step with the specified name, or undefined if not found. A step data object has the |
findStep: function(name) {
return _.findWhere(this.steps, { name: name });
},
|
¶ #getNextStep()Returns the next step that will be executed.
That is either the step that was specified with Returns undefined if there are no more steps to run. |
getNextStep: function() {
var nextStep = this.nextStep;
delete this.nextStep;
return nextStep || this.steps[_.indexOf(this.steps, this.currentStep) + 1];
},
|
¶ Run AlgorithmSteps are executed using promises with the q library. This allows steps to be asynchronous and thrown errors to be caught by the promise chain. |
|
¶ #run(options)Runs this scenario with the specified runtime options. Available options are the same as for the constructor. Returns a promise that is resolved if the scenario completed successfully, or rejected if any step fails and it was interrupted. |
run: function(options) {
|
An error is thrown if no steps are defined. |
if (!this.steps.length) {
throw new Error('No step defined');
}
|
Run options are built by overriding construction options with runtime options. Runtime options come from the command line and/or configuration file; they are handled in cli.command.js. |
var runOptions = merge(this.baseOptions, options || {});
_.defaults(runOptions, this.defaultRunOptions);
var promise = q(runOptions);
_.each(this.beforeRun, function(pre) {
promise = promise.then(_.bind(pre, this)).then(_.bind(q, undefined, runOptions));
}, this);
|
Runtime parameters are loaded and validated before actually running the scenario. This is handled in scenario.ext.params.js. |
return promise.then(_.bind(this.runScenario, this, runOptions));
},
validateRunOptions: function(runOptions) {
if (_.has(runOptions, 'log') && (!_.isString(runOptions.log) || !_.contains(LOG_LEVELS, runOptions.log.toLowerCase()))) {
throw new Error('Unknown log level "' + runOptions.log + '"; must be one of ' + LOG_LEVELS.join(', '));
}
return runOptions;
},
runScenario: function(runOptions) {
|
EVENT: the |
this.configure(runOptions);
|
EVENT: the |
this.emit('scenario:start', runOptions);
var deferred = q.defer();
|
The scenario always starts with the first step. |
this.runStep(_.first(this.steps), deferred);
|
The returned promise will be resolved when the last step of the scenario is done executing, or if a step fails and the scenario is interrupted. |
return deferred.promise;
},
|
¶ #runStep(step, deferred, stepArgs...)Runs the specified step. The second argument is the deferred object that must be resolved or rejected to complete the scenario. The remaining arguments should be passed to the step definition function. |
runStep: function(step, deferred) {
this.currentStep = step;
delete this.stepSkipped;
delete this.skipMessage;
|
The step description object is passed to all step events; it contains the name of the step that is being executed. |
var description = { name: step.name },
stepArgs = slice.call(arguments, 2);
|
EVENT: the |
this.emit.apply(this, [ 'step:start', description ].concat(stepArgs));
|
The step definition function is called with this scenario as the context and with the step arguments. This is done with the q library so a promise is returned and any errors are handled by the promise chain. |
var stepResult = q.fapply(_.bind(step.definition, this), stepArgs);
|
|
stepResult.then(_.bind(this.handleStepResult, this, deferred, description), _.bind(this.handleStepError, this, deferred, description));
},
|
¶ #handleStepResult(deferred, description, previousStepResults...)Executes the next step or completes the scenario. |
handleStepResult: function(deferred, description) {
|
The results from the previous steps are given as additional arguments to this method.
If the result is a single |
var results = slice.call(arguments, 2);
if (results.length == 1 && results[0] instanceof StepArgs) {
results = results[0].args;
}
if (this.stepSkipped) {
|
EVENT: the |
this.emit.apply(this, [ 'step:skip', description, this.skipMessage ].concat(results));
} else {
|
EVENT: the |
this.emit.apply(this, [ 'step:done', description ].concat(results));
}
var nextStep = this.getNextStep();
|
The scenario is stopped if there are no more steps to run or if it was marked as completed. |
if (!nextStep || this.completed) {
|
EVENT: the |
this.emit('scenario:end');
return deferred.resolve();
}
|
The next step is run with the results from the previous step as arguments. |
this.runStep.apply(this, [ nextStep, deferred ].concat(results));
},
|
¶ #handleStepError(deferred, description, errorMessage)Rejects the scenario deferred object with the specified error message. |
handleStepError: function(deferred, description, err) {
|
EVENT: the |
this.emit('step:error', description, err);
|
EVENT: the |
this.emit('scenario:error', err);
deferred.reject(err);
}
});
|
TODO: document extensions |
_.each(extensions, function(ext) {
ext(Scenario.prototype);
});
|
Utility class to hold multiple step arguments.
See |
function StepArgs(args) {
this.args = args;
}
return Scenario;
};
module.exports['@singleton'] = true;
module.exports['@require'] = [ 'scenario.ext', 'log4js', 'cli.print' ];
|