Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Cucumber features runner complete; no scenario outlines

  • Loading branch information...
commit 16c293eb1892620d05744bcc135af4337136c4db 1 parent 7f57fa2
@pacovell pacovell authored
View
199 bin/kyuri
@@ -5,22 +5,9 @@ var path = require('path'),
fs = require('fs'),
events = require('events');
-//
-// Attempt to load Coffee-Script. If it's not available, continue on our
-// merry way, if it is available, set it up so we can include `*.coffee`
-// scripts and start searching for them.
-//
-var fileExt, specFileExt;
-
-try {
- var coffee = require('coffee-script');
- require.registerExtension('.coffee', function (content) { return coffee.compile(content) });
- stepExt = /\.(js|coffee)$/;
-} catch (_) {
- fileExt = /\.js$/;
-}
-
featureFileExt = /\.(feature)$/;
+stepsDirs = ['steps', 'step_definitions'];
+envDirs = ['support'];
var inspect = require('eyes').inspector({
stream: null,
@@ -32,125 +19,83 @@ require.paths.unshift(path.join(__dirname, '..', 'lib'));
var kyuri = require('kyuri');
var help = [
- "usage: kyuri [FILE, ...] [options]",
- "",
- "options:",
- // Options compliant from 'cucumber --help'
- " --i18n LANG List keywords for in a particular language",
- " Run with \"--i18n help\" to see all languages",
- " -h, --help You're staring at it",
-
- // Options specific to Kyuri
- " --server Run the Kyuri web service. Default to port 9000",
- " --port Port to run server on",
-
+ "usage: kyuri [FILE, ...]"
].join('\n');
-/* Lets be compliant with Cucumber since this is essentially a pure javascript runner + some javascript options foo?
- (i.e. lets generate .coffee vs. .js step definitions :])
-
+var root = process.cwd();
- -r, --require LIBRARY|DIR Require files before executing the features. If this
- option is not specified, all *.rb files that are
- siblings or below the features will be loaded auto-
- matically. Automatic loading is disabled when this
- option is specified, and all loading becomes explicit.
- Files under directories named "support" are always
- loaded first.
- This option can be specified multiple times.
- --i18n LANG List keywords for in a particular language
- Run with "--i18n help" to see all languages
- -f, --format FORMAT How to format features (Default: pretty). Available formats:
- debug : For developing formatters - prints the calls made to the listeners.
- html : Generates a nice looking HTML report.
- json : Prints the feature as JSON
- json_pretty : Prints the feature as pretty JSON
- junit : Generates a report similar to Ant+JUnit.
- pdf : Generates a PDF report. You need to have the
- prawn gem installed. Will pick up logo from
- features/support/logo.png or
- features/support/logo.jpg if present.
- pretty : Prints the feature as is - in colours.
- progress : Prints one character per scenario.
- rerun : Prints failing files with line numbers.
- stepdefs : Prints All step definitions with their locations. Same as
- the usage formatter, except that steps are not printed.
- tag_cloud : Prints a tag cloud of tag usage.
- usage : Prints where step definitions are used.
- The slowest step definitions (with duration) are
- listed first. If --dry-run is used the duration
- is not shown, and step definitions are sorted by
- filename instead.
- Use --format rerun --out features.txt to write out failing
- features. You can rerun them with cucumber @rerun.txt.
- FORMAT can also be the fully qualified class name of
- your own custom formatter. If the class isn't loaded,
- Cucumber will attempt to require a file with a relative
- file name that is the underscore name of the class name.
- Example: --format Foo::BarZap -> Cucumber will look for
- foo/bar_zap.rb. You can place the file with this relative
- path underneath your features/support directory or anywhere
- on Ruby's LOAD_PATH, for example in a Ruby gem.
- -o, --out [FILE|DIR] Write output to a file/directory instead of STDOUT. This option
- applies to the previously specified --format, or the
- default format if no format is specified. Check the specific
- formatter's docs to see whether to pass a file or a dir.
- -t, --tags TAG_EXPRESSION Only execute the features or scenarios with tags matching TAG_EXPRESSION.
- Scenarios inherit tags declared on the Feature level. The simplest
- TAG_EXPRESSION is simply a tag. Example: --tags @dev. When a tag in a tag
- expression starts with a ~, this represents boolean NOT. Example: --tags ~@dev.
- A tag expression can have several tags separated by a comma, which represents
- logical OR. Example: --tags @dev,@wip. The --tags option can be specified
- several times, and this represents logical AND. Example: --tags @foo,~@bar --tags @zap.
- This represents the boolean expression (@foo || !@bar) && @zap.
-
- Beware that if you want to use several negative tags to exclude several tags
- you have to use logical AND: --tags ~@fixme --tags @buggy.
-
- Positive tags can be given a threshold to limit the number of occurrences.
- Example: --tags @qa:3 will fail if there are more than 3 occurrences of the @qa tag.
- This can be practical if you are practicing Kanban or CONWIP.
- -n, --name NAME Only execute the feature elements which match part of the given name.
- If this option is given more than once, it will match against all the
- given names.
- -e, --exclude PATTERN Don't run feature files or require ruby files matching PATTERN
- -p, --profile PROFILE Pull commandline arguments from cucumber.yml which can be defined as
- strings or arrays. When a 'default' profile is defined and no profile
- is specified it is always used. (Unless disabled, see -P below.)
- When feature files are defined in a profile and on the command line
- then only the ones from the command line are used.
- -P, --no-profile Disables all profile loading to avoid using the 'default' profile.
- -c, --[no-]color Whether or not to use ANSI color in the output. Cucumber decides
- based on your platform and the output destination if not specified.
- -d, --dry-run Invokes formatters without executing the steps.
- This also omits the loading of your support/env.rb file if it exists.
- Implies --no-snippets.
- -a, --autoformat DIR Reformats (pretty prints) feature files and write them to DIRECTORY.
- Be careful if you choose to overwrite the originals.
- Implies --dry-run --formatter pretty.
- -m, --no-multiline Don't print multiline strings and tables under steps.
- -s, --no-source Don't print the file and line of the step definition with the steps.
- -i, --no-snippets Don't print snippets for pending steps.
- -q, --quiet Alias for --no-snippets --no-source.
- -b, --backtrace Show full backtrace for all errors.
- -S, --strict Fail if there are any undefined steps.
- -w, --wip Fail if there are any passing scenarios.
- -v, --verbose Show the files and features loaded.
- -g, --guess Guess best match for Ambiguous steps.
- -x, --expand Expand Scenario Outline Tables in output.
- --drb Run features against a DRb server. (i.e. with the spork gem)
- --port PORT Specify DRb port. Ignored without --drb
- --version Show version.
- -h, --help You're looking at it.
-
+/**
+ Load .js files found in the directory or subdirectories of the feature files,
+ adding exports to the steps array (if any).
*/
+var _loadJavascripts = function (directory, steps) {
+ if (!directory.match(/^\//)) {
+ directory = path.join(root, directory);
+ }
+
+ if(path.existsSync(directory)) {
+ var files = fs.readdirSync(directory);
+ files.forEach(function (file) {
+ var fullPath = path.join(directory, file),
+ stats = fs.statSync(fullPath),
+ exported;
+
+ if (stats.isDirectory()) {
+ _loadJavascripts(fullPath, steps);
+ } else {
+ if (file.match(/\.js$/)) {
+ // Add root to all relative paths
+ if (!fullPath.match(/^\//)) {
+ fullPath = path.join(root, fullPath);
+ }
+ console.log('Loaded ' + fullPath);
+ exported = require(fullPath);
+ if (exported && exported.forEach && steps) {
+ exported.forEach(function (obj) {
+ steps.push(obj); // Pass-by-reference array must be modified in place
+ })
+ }
+ }
+ }
+ });
+ }
+};
// Get rid of process runner
// ('node' in most cases)
var arg, args = [], argv = process.argv.slice(2);
-// Current directory index,
-// and path of test folder.
-var root, testFolder;
+kyuri.runner = kyuri.runners.cucumber;
+
+var features = [];
+var steps = [];
+argv.forEach(function (file) {
+ if (file.match(featureFileExt)) {
+ // Load envDirs first and without steps
+ envDirs.forEach(function (dir) {
+ _loadJavascripts(path.join(path.dirname(file), dir));
+ });
+ // Load steps
+ stepsDirs.forEach(function (dir) {
+ _loadJavascripts(path.join(path.dirname(file), dir), steps);
+ });
+ features.push(kyuri.parse(fs.readFileSync(file).toString()));
+ }
+});
+
+var complete = false;
+
+kyuri.runners.cucumber.run(features, steps, function (err) {
+ if (err) {
+ console.log('Errors');
+ console.log(err);
+ }
+ complete = true;
+});
-sys.puts('Kyuri test runner not currently complete in 0.2.0. In the roadmap for 0.2.1');
+var _waitComplete = function () {
+ if (!complete) {
+ process.nextTick(_waitComplete);
+ }
+};
+_waitComplete();
View
8 examples/step_definitions/calculator.js
@@ -0,0 +1,8 @@
+var Steps = require('kyuri').Steps;
+
+Steps.Given(/^I have entered (\d+) into the calculator$/, function (step, num) {
+ console.log('Calculator: ' + num);
+ step.done();
+});
+
+Steps.export(module);
View
18 examples/support/env.js
@@ -0,0 +1,18 @@
+/*
+ * Set up environmental configuration here, for example database events and functions
+ *
+ * (C) 2011 Paul Covell (paul@done.com)
+ * MIT LICENSE
+ *
+ */
+var Runner = require('kyuri').runner;
+
+Runner.on('beforeTest', function (done) {
+ console.log('beforeTest event');
+ done();
+});
+
+Runner.on('afterTest', function (done) {
+ console.log('afterTest event');
+ done();
+});
View
1  lib/kyuri.js
@@ -27,6 +27,7 @@ kyuri.Steps = require('kyuri/steps');
//
kyuri.runners = {};
kyuri.runners.vows = require('kyuri/runners/vows');
+kyuri.runners.cucumber = require('kyuri/runners/cucumber');
//
// Remark we should probably export the runner methods
View
141 lib/kyuri/runners/cucumber.js
@@ -0,0 +1,141 @@
+/*
+ * cucumber.js: Methods for directly running features against a Cucumber layout.
+ *
+ * (C) 2011 Paul Covell (paul@done.com)
+ * MIT LICENSE
+ *
+ */
+var kyuri = require('kyuri'),
+ fs = require('fs'),
+ util = require('util'),
+ EventEmitter = require('events').EventEmitter;
+
+var Cucumber = function () {
+ EventEmitter.call(this);
+ this.missingSteps = [];
+};
+util.inherits(Cucumber, EventEmitter);
+
+/**
+ Runs the parsed feature files provided with the steps
+*/
+Cucumber.prototype.run = function (features, steps, callback) {
+ var self = this;
+
+ self._emitAndWait('beforeTest', function () {
+ self._invokeSerial(features, function (feature, featureCb) {
+ var feature = feature[Object.keys(feature).shift()];
+ self._invokeSerial(feature.scenarios, function (scenario, scenarioCb) {
+ if (scenario.outline) {
+ scenarioCb();
+ } else {
+ self._invokeSerial(scenario.breakdown, function(step, stepCb) {
+ var step = step[Object.keys(step).shift()],
+ text = step.join(' ');
+
+ self._executeStepDefinition(steps, step[1], stepCb);
+ }, scenarioCb);
+ }
+ }, featureCb);
+ },
+ function (err) {
+ self._emitAndWait('afterTest', function() {
+ if (self.missingSteps.length > 0) {
+ console.log('Missing Steps');
+ console.log(self.missingSteps.join('\n'));
+ }
+ callback(err);
+ });
+ });
+ });
+};
+
+/**
+ Run the matching step definition, if any
+*/
+Cucumber.prototype._executeStepDefinition = function (steps, step, callback) {
+ var stepContext, fn, matches;
+ var self = this;
+
+ steps.forEach(function (rule) {
+ if (!fn) {
+ matches = step.match(rule.pattern);
+ if (matches) {
+ fn = rule.generator;
+ } else {
+ if (self.missingSteps.indexOf(step) === (-1)) {
+ self.missingSteps.push(step);
+ }
+ }
+ };
+ });
+
+ stepContext = {
+ done : callback,
+ pending : callback,
+ };
+
+ if (fn) {
+ matches = matches.slice(1);
+ matches.unshift(stepContext);
+ fn.apply(this, matches);
+ return true;
+ } else {
+ stepContext.pending();
+ }
+};
+
+/**
+ Map function over each item in the array in order, calling callback when complete
+ fn = function (item, callback)
+*/
+Cucumber.prototype._invokeSerial = function (ar, fn, callback) {
+ (function (ar, fn, callback) {
+ var context = this,
+ i = -1;
+
+ function _callback(err) {
+ i += 1;
+ if (!err && i < ar.length) {
+ fn.call(context, ar[i], _callback);
+ } else {
+ callback(err);
+ }
+ };
+
+ _callback();
+ }).call(this, ar, fn, callback);
+};
+
+Cucumber.prototype._emitAndWait = function (event, callback) {
+ var count = this.listeners(event).length;
+
+ this.emit(event, function () {
+ count -= 1;
+ if (count === 0) {
+ callback();
+ }
+ });
+};
+
+/**
+ Invoke function with all items in the array, calling callback when complete
+ fn = function (item, callback)
+*/
+Cucumber.prototype._invokeParallel = function (ar, fn, callback) {
+ (function (ar, fn, callback) {
+ var context = this,
+ count = 0;
+
+ ar.forEach(function (item) {
+ fn.call(context, item, function (err) {
+ count += 1;
+ if (count >= ar.length) {
+ callback();
+ }
+ });
+ });
+ }).call(this, ar, fn, callback);
+};
+
+module.exports = new Cucumber();
Please sign in to comment.
Something went wrong with that request. Please try again.