Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Cuke runner rc #19

Merged
merged 9 commits into from

5 participants

@pacovell

Also small changes to support Node 0.6.x

pacovell and others added some commits
@pacovell pacovell Cucumber features runner complete; no scenario outlines 16c293e
@eugeneware eugeneware Adding 'Background' Support 00650eb
@pacovell pacovell Added background calls, beforeBackground event
Rearranged outline parsing (now produces more friendly hash structures) and implemented outline execution in cucumber runner
1fe714c
@rajkissu rajkissu 'And' keyword now usable for writing step definitions 63291f8
@rajkissu rajkissu Added tabspace option, defaults to 8. This allows us to use spaces in…
…stead of tabs for indentation without crashing the lexer
b9dbf61
@pacovell pacovell Allow lexing to process '@' within steps - for example, if you have e…
…mail addresses
527b9c0
@pacovell pacovell Improved runtime error handling of cucumber runner
- Features now continue on error so you can run multiple files
- timeout feature to step running
Improved logging output for cucumber runner
- Print error and pending step information
- Can now accept directory names for running features
Added bin to package.json
9cdc403
@pacovell pacovell Improved lexer for Scenario Outlines ea22361
@pacovell pacovell Node 0.6.x - removed require.paths, changed sys to util 477b3e8
@indexzero
Owner

@pacovell Nice! I'll give this a look over tonight and merge it in :-D

@pacovell

Great - it isn't as perfect as I'd like, but I have been using it for a while now, so it works

@indexzero indexzero merged commit ac59608 into from
@marco4net

should be conditional on isText, i.e.
if (isText)
code = code.replace(new RegExp(' {' + options.tabspace + '}', 'g'), '\t');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 5, 2011
  1. @pacovell
  2. @eugeneware @pacovell

    Adding 'Background' Support

    eugeneware authored pacovell committed
  3. @pacovell

    Added background calls, beforeBackground event

    pacovell authored
    Rearranged outline parsing (now produces more friendly hash structures) and implemented outline execution in cucumber runner
  4. @rajkissu @pacovell

    'And' keyword now usable for writing step definitions

    rajkissu authored pacovell committed
  5. @rajkissu @pacovell

    Added tabspace option, defaults to 8. This allows us to use spaces in…

    rajkissu authored pacovell committed
    …stead of tabs for indentation without crashing the lexer
  6. @pacovell
  7. @pacovell

    Improved runtime error handling of cucumber runner

    pacovell authored
    - Features now continue on error so you can run multiple files
    - timeout feature to step running
    Improved logging output for cucumber runner
    - Print error and pending step information
    - Can now accept directory names for running features
    Added bin to package.json
  8. @pacovell
  9. @pacovell
This page is out of date. Refresh to see the latest.
View
235 bin/kyuri
@@ -1,156 +1,129 @@
#!/usr/bin/env node
var path = require('path'),
- sys = require('sys'),
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,
styles: { string: 'grey', regexp: 'grey' }
});
-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 :])
-
-
- -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.
-
+var root = process.cwd();
+
+/**
+ 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);
+ }
+ 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 = [];
+var directories = [];
+
+argv.forEach(function (file) {
+ var stat = fs.statSync(file),
+ files;
+
+ if (stat.isDirectory()) {
+ files = fs.readdirSync(file);
+ for (var i = 0; i < files.length; i++) {
+ // Add the path
+ files[i] = path.join(file, files[i]);
+ }
+ } else {
+ files = [file];
+ }
+
+ files.forEach(function (file) {
+ if (file.match(featureFileExt)) {
+ // Load envDirs first and without steps
+ if (directories.indexOf(path.dirname(file)) === (-1)) {
+ directories.push(path.dirname(file));
+ }
+ features.push(kyuri.parse(fs.readFileSync(file).toString()));
+ }
+ });
+});
+
+directories.forEach(function (top) {
+ // Load environments first
+ envDirs.forEach(function (dir) {
+ _loadJavascripts(path.join(top, dir));
+ });
+
+ // Load steps
+ stepsDirs.forEach(function (dir) {
+ _loadJavascripts(path.join(top, dir), steps);
+ });
+});
+
+
+var complete = false;
+
+try {
+ kyuri.runners.cucumber.run(features, steps, function () {
+ complete = true;
+ });
+} catch (err) {
+ console.log('Errors');
+ if (err.stack) {
+ console.log(err.stack);
+ } else {
+ 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/complex.feature
@@ -2,9 +2,12 @@ Feature: Complex Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers
-
+
+ Background:
+ Given I have a calculator
+
Scenario: Add two numbers
- Given I have entered 50 into the calculator
+ Given I have entered "50" and "75" into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen
@@ -20,7 +23,6 @@ Feature: Complex Addition
| number1 | number2 | number3 |
| 10 | 20 | 150 |
| 20 | 40 | 180 |
- | 40 | 60 | 220 |
Scenario: Add two numbers
Given I have entered 50 into the calculator
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
24 examples/support/env.js
@@ -0,0 +1,24 @@
+/*
+ * 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('beforeBackground', function (done) {
+ console.log('beforeBackground event');
+ done();
+});
+
+Runner.on('afterTest', function (done) {
+ console.log('afterTest event');
+ done();
+});
+
View
19 lib/kyuri.js
@@ -6,27 +6,26 @@
*
*/
-require.paths.unshift(__dirname);
-
var kyuri = exports;
//
// Export core methods
//
kyuri.version = '0.1.0';
-kyuri.compile = require('kyuri/core').compile;
-kyuri.parse = require('kyuri/core').parse;
-kyuri.tokens = require('kyuri/core').tokens;
-kyuri.nodes = require('kyuri/core').nodes;
-kyuri.setLanguage = require('kyuri/core').setLanguage;
-kyuri.i18n = require('kyuri/core').i18n;
-kyuri.Steps = require('kyuri/steps');
+kyuri.compile = require('./kyuri/core').compile;
+kyuri.parse = require('./kyuri/core').parse;
+kyuri.tokens = require('./kyuri/core').tokens;
+kyuri.nodes = require('./kyuri/core').nodes;
+kyuri.setLanguage = require('./kyuri/core').setLanguage;
+kyuri.i18n = require('./kyuri/core').i18n;
+kyuri.Steps = require('./kyuri/steps');
//
// Export runners
//
kyuri.runners = {};
-kyuri.runners.vows = require('kyuri/runners/vows');
+kyuri.runners.vows = require('./kyuri/runners/vows');
+kyuri.runners.cucumber = require('./kyuri/runners/cucumber');
//
// Remark we should probably export the runner methods
View
9 lib/kyuri/core.js
@@ -25,6 +25,13 @@ exports.compile = function (code, options) {
// Default options are to generate steps only
options = options || {};
options.target = options.target || 'steps'
+ options.tabspace = options.tabspace || 8;
+
+ // Convert groups of spaces to tabs
+ code = code.replace(new RegExp(' {' + options.tabspace + '}', 'g'), '\t');
+
+ // we don't need this anymore
+ delete options.tabspace;
try {
ast = isText ? parser.parse(lexer.tokenize(code)) : code;
@@ -64,4 +71,4 @@ exports.setLanguage = function (language) {
lexer = new Lexer(language, i18n);
};
-exports.i18n = i18n;
+exports.i18n = i18n;
View
4 lib/kyuri/generator.js
@@ -10,7 +10,7 @@ var _ = require('underscore')._,
fs = require('fs'),
path = require('path'),
eyes = require('eyes'),
- sys = require('sys');
+ util = require('util');
// Configure underscore to work like mustache
_.templateSettings = {
@@ -185,7 +185,7 @@ var StepGenerator = function (ast) {
};
// Make 'StepGenerator' inherit from 'Generator'
-sys.inherits(StepGenerator, Generator);
+util.inherits(StepGenerator, Generator);
exports.Generator = Generator;
exports.StepGenerator = StepGenerator;
View
5 lib/kyuri/lexer.js
@@ -7,15 +7,14 @@
*/
var helpers = require('./helpers'),
- sys = require('sys'),
eyes = require('eyes');
var MULTI_DENT = /^(\t+)(\.)?/,
IS_EXAMPLE_ROW = /^([\|\s+\S+]+\s+\|\s*)$/,
- PARSE_EXAMPLE_ROW = /\|\s*(\S+)/gi,
+ PARSE_EXAMPLE_ROW = /\|[^|]+/gi,
PYSTRING = /"""/,
TAGS_LENGTH = /[@\w+\s*]+/i,
- TAGS = /@(\w+)/gi,
+ TAGS = /(?:^@|\s+@)(\w+)/gi,
COMMENT = /#\s*([\S+\s+]+)/i,
SENTENCE = /([\S+\s+]+)/i,
SENTENCE_COMMENT = /([\S+\s+]+)#\s*([\S+\s+]+)/i;
View
56 lib/kyuri/parser.js
@@ -6,9 +6,6 @@
*
*/
-var sys = require('sys'),
- eyes = require('eyes');
-
//
// Helper method to get the last feature
// (i.e. the current feature we are building the ast for)
@@ -24,7 +21,11 @@ var getLastFeature = function (ast) {
//
var getLastScenario = function (ast) {
var feature = getLastFeature(ast);
- return feature.scenarios[feature.scenarios.length - 1];
+ if (feature.background && feature.background.background) {
+ return feature.background;
+ } else {
+ return feature.scenarios[feature.scenarios.length - 1];
+ }
};
//
@@ -98,7 +99,8 @@ var _states = {
ast[id.toString()] = {
name: token[1],
description: '',
- scenarios: []
+ scenarios: [],
+ background: null
};
return ast[id.toString()];
}
@@ -138,12 +140,36 @@ var _states = {
return feature;
}
},
+ 'BACKGROUND': {
+ value: 'BACKGROUND',
+ next: 'scenario',
+ last: ['TERMINATOR', 'OUTDENT'],
+ build: function (ast, token) {
+ var feature = getLastFeature(ast);
+
+ if (feature.background) {
+ throw new Error('Only one Background per feature is allowed');
+ }
+
+ feature.background = {
+ background: true,
+ outline: false,
+ breakdown: [],
+ };
+ return feature;
+ }
+ },
'SCENARIO': {
value: 'SCENARIO',
next: 'scenario',
last: ['TERMINATOR', 'OUTDENT'],
build: function (ast, token) {
var feature = getLastFeature(ast);
+
+ if (feature.background && feature.background.background) {
+ delete feature.background.background;
+ }
+
feature.scenarios.push({
outline: false,
breakdown: [],
@@ -180,7 +206,12 @@ var _states = {
scenario.name = token[1];
return scenario;
}
- }
+ },
+ 'TERMINATOR': {
+ value: '*',
+ next: 'scenarioHeader',
+ last: ['BACKGROUND', 'SCENARIO', 'SCENARIO_OUTLINE'],
+ },
}
},
@@ -287,19 +318,20 @@ var _states = {
next: 'exampleRows',
last: ['INDENT', 'TERMINATOR'],
build: function (ast, token) {
- var scenario = getLastScenario(ast);
+ var scenario = getLastScenario(ast),
+ example;
+
if(!scenario.hasExamples) {
- for (var i = 0; i < token[1].length; i++) {
- scenario.examples[token[1][i]] = [];
- }
-
+ scenario.examples = [];
scenario.exampleVariables = token[1];
scenario.hasExamples = true;
}
else {
+ example = {};
for (var i = 0; i < token[1].length; i++) {
- scenario.examples[scenario.exampleVariables[i]].push(token[1][i]);
+ example[scenario.exampleVariables[i]] = token[1][i];
}
+ scenario.examples.push(example);
}
return scenario;
}
View
362 lib/kyuri/runners/cucumber.js
@@ -0,0 +1,362 @@
+/*
+ * 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'),
+ colors = require('colors'),
+ lingo = require('lingo'),
+ EventEmitter = require('events').EventEmitter;
+
+var log = console.log;
+
+var Cucumber = function () {
+ EventEmitter.call(this);
+ this.missingSteps = {};
+ this.scenarios = {
+ total: 0,
+ passed: 0,
+ failed: 0
+ };
+ this.steps = {
+ total: 0,
+ passed: 0,
+ pending: 0,
+ undefined: 0,
+ failed: 0
+ };
+ this.runtime = {
+ start: 0,
+ stop: 0
+ }
+};
+util.inherits(Cucumber, EventEmitter);
+
+/**
+ Runs the parsed feature files provided with the steps
+*/
+Cucumber.prototype.run = function (features, steps, callback) {
+ var self = this;
+
+ function runFeatures (features, next) {
+ self._invokeSerial(features, function (feature, featureCb) {
+ var feature = feature[Object.keys(feature).shift()];
+
+ log(('Feature: ' + feature.name).green);
+ log(self._withIndent(1, feature.description).green);
+
+ if (feature.background) {
+ log(self._withIndent(1, 'Background:').green);
+ feature.background.breakdown.forEach(function (step) {
+ var step = step[Object.keys(step).shift()];
+ log(self._withIndent(2, self._formatStep(step)).green);
+ });
+ }
+
+ runScenarios(feature, featureCb);
+ }, next, { continueOnError: true });
+ }
+
+ function runBackground (feature, next) {
+ if (feature.background) {
+ self._emitAndWait('beforeBackground', function() {
+ self._invokeSerial(feature.background.breakdown, runStep, next);
+ });
+ } else {
+ next();
+ }
+ }
+
+ function runScenarios (feature, next) {
+
+ function complete(callback) {
+ return function (err) {
+ if (err) {
+ self.scenarios.failed += 1;
+ } else {
+ self.scenarios.passed += 1;
+ }
+ callback(err);
+ }
+ }
+
+ self._invokeSerial(feature.scenarios, function (scenario, scenarioCb) {
+ self.scenarios.total += 1;
+
+ log('');
+
+ if (scenario.outline) {
+
+ log(self._withIndent(1, 'Scenario Outline: ' + scenario.name).green);
+ log('');
+
+ self._invokeSerial(scenario.examples, function (example, exampleCb) {
+ var steps = []
+
+ log(self._withIndent(1, 'Example').green);
+
+ // Create customized steps by replacing the template steps with
+ // the example variables
+ scenario.breakdown.forEach(function (step) {
+ var exampleStep = {};
+ Object.keys(step).forEach(function (i) {
+ exampleStep[i] = step[i].slice(0); // copy the array
+ scenario.exampleVariables.forEach(function (variable) {
+ exampleStep[i][1] = exampleStep[i][1].replace('\<' + variable + '\>', example[variable]);
+ });
+ });
+ steps.push(exampleStep);
+ });
+
+ runBackground(feature, function (err) {
+ self._invokeSerial(steps, runStep, exampleCb);
+ });
+ }, complete(scenarioCb));
+ } else {
+
+ log(self._withIndent(1, 'Scenario: ' + scenario.name).green);
+
+ runBackground(feature, function (err) {
+ self._invokeSerial(scenario.breakdown, runStep, complete(scenarioCb));
+ });
+ }
+ }, next);
+ }
+
+ function runStep (step, next) {
+ var step = step[Object.keys(step).shift()];
+ try {
+ self._executeStepDefinition(steps, step, next);
+ } catch (err) {
+ console.log('Caught Exception'.red);
+ console.log(err.stack);
+ next(err);
+ }
+ }
+
+ self._emitAndWait('beforeTest', function () {
+ self.runtime.start = new Date();
+ runFeatures(features, function (err) {
+ self.runtime.stop = new Date();
+ self._emitAndWait('afterTest', function() {
+ self._printSummary();
+ callback(err);
+ });
+ });
+ });
+};
+
+/**
+ Print test run summary information
+*/
+Cucumber.prototype._printSummary = function () {
+ var self = this,
+ stepResults = [],
+ scenarioResults = [],
+ totalTime, ms;
+
+ function formatStep(type, color) {
+ if (self.steps[type]) {
+ stepResults.push((self.steps[type] + ' ' + type)[color]);
+ }
+ }
+
+ function formatScenario(type, color) {
+ if (self.scenarios[type]) {
+ scenarioResults.push((self.scenarios[type] + ' ' + type)[color]);
+ }
+ }
+
+ formatStep('passed', 'green');
+ formatStep('undefined', 'yellow');
+ formatStep('pending', 'yellow');
+ formatStep('failed', 'red');
+
+ formatScenario('passed', 'green');
+ formatScenario('failed', 'red');
+
+ log('');
+ log(this.scenarios.total + ' scenarios (' + scenarioResults.join(', ') + ')');
+ log(this.steps.total + ' steps (' + stepResults.join(', ') + ')');
+
+ totalTime = this.runtime.stop - this.runtime.start;
+ ms = (totalTime % 1000) / 1000;
+
+ log(Math.floor(totalTime / 1000 / 60) + 'm' + Math.floor(totalTime / 1000) + '.' + (ms === 0 ? '000' : ms.toString().slice(2)) + 's');
+
+ this._printMissingSteps();
+};
+
+Cucumber.prototype._printMissingSteps = function () {
+ var self = this,
+ formattedMissingSteps = {};
+
+ function _yellow(text) {
+ log(text.yellow);
+ }
+
+ function _replaceVars(text) {
+ var update;
+
+ update = text.replace(/\d+/g, '(\\d+)');
+ update = update.replace(/\"[^"]*?\"/g, '"([^"]*?)"');
+ return update;
+ }
+
+ if (Object.keys(self.missingSteps).length > 0) {
+ // Prepare the text output, and also ensure that we don't print steps that resolve
+ // to the same pattern more than once by indexing the new hash on the pattern
+ Object.keys(self.missingSteps).forEach(function (key) {
+ var step = self.missingSteps[key],
+ pattern = '/^' + _replaceVars(step[1]) + '$/',
+ fn = lingo.camelcase(step[0].toString().toLowerCase(), true),
+ args = ['step'],
+ matches;
+
+ matches = pattern.match(/\([^)]*\)/g);
+ if (matches) {
+ for(var i = 0; i < matches.length; i++) {
+ args.push('arg' + (i + 1));
+ }
+ }
+ formattedMissingSteps[pattern] = { fn: fn, args: args };
+ });
+
+ _yellow('');
+ _yellow('You can implement step definitions for undefined steps with these snippets:');
+ _yellow('');
+ _yellow("var Steps = require('kyuri').Steps;");
+ _yellow('');
+ Object.keys(formattedMissingSteps).forEach(function (pattern) {
+ var step = formattedMissingSteps[pattern];
+ _yellow('Steps.' + step.fn + '(' + pattern + ', function (' + step.args.join(', ') + ') {');
+ _yellow('\tstep.pending();');
+ _yellow('});')
+ _yellow('');
+ });
+ _yellow('Steps.export(module);');
+ }
+};
+
+/**
+ Run the matching step definition, if any
+*/
+Cucumber.prototype._executeStepDefinition = function (steps, step, callback) {
+ var self = this,
+ timeoutId,
+ stepContext, fn, matches,
+ stepText = self._formatStep(step);
+
+ steps.forEach(function (rule) {
+ if (!fn) {
+ matches = step[1].match(rule.pattern);
+ if (matches) {
+ fn = rule.generator;
+ }
+ };
+ });
+
+ stepContext = {
+ done : function (err) {
+ clearTimeout(timeoutId);
+ if (err) {
+ log(self._withIndent(2, stepText).red);
+ log(self._withIndent(2, err.toString().red));
+ self.steps.failed += 1;
+ } else {
+ log(self._withIndent(2, stepText).green);
+ self.steps.passed += 1;
+ }
+ callback(err);
+ },
+ pending : function () {
+ clearTimeout(timeoutId);
+ log(self._withIndent(2, stepText).yellow);
+ log(self._withIndent(2, '(PENDING)').yellow);
+ self.steps.pending += 1;
+ callback();
+ }
+ };
+
+ self.steps.total += 1;
+ if (fn) {
+ matches = matches.slice(1);
+ matches.unshift(stepContext);
+
+ timeoutId = setTimeout(function () {
+ stepContext.done('Timed out');
+ }, 5000);
+
+ fn.apply(this, matches);
+ } else {
+ self.steps.undefined += 1;
+ self.missingSteps[step.join(' ')] = step;
+ log(self._withIndent(2, stepText).yellow);
+ callback();
+ }
+};
+
+/**
+ Map function over each item in the array in order, calling callback when complete
+ fn = function (item, callback)
+ options:
+ continueOnError : true if you want the execution to continue even if there's an error
+ (will still stop on exception)
+*/
+Cucumber.prototype._invokeSerial = function (ar, fn, callback, options) {
+ (function (ar, fn, callback, options) {
+ var context = this,
+ i = -1;
+
+ options = options || {};
+
+ function _callback(err) {
+ i += 1;
+ if (i >= ar.length || (err && options.continueOnError !== true)) {
+ callback(err);
+ } else {
+ fn.call(context, ar[i], _callback);
+ }
+ };
+
+ _callback();
+ }).call(this, ar, fn, callback, options);
+};
+
+/**
+ Emit the event and wait for all listeners to call the callback
+*/
+Cucumber.prototype._emitAndWait = function (event, callback) {
+ var count = this.listeners(event).length;
+
+ if (count === 0) {
+ callback();
+ } else {
+ this.emit(event, function () {
+ count -= 1;
+ if (count === 0) {
+ callback();
+ }
+ });
+ }
+};
+
+Cucumber.prototype._formatStep = function (step) {
+ return lingo.camelcase(step[0].toLowerCase(), true) + ' ' + step[1];
+};
+
+Cucumber.prototype._withIndent = function (count, str) {
+ var indent = '';
+
+ for (var i = 0; i < count; i++) {
+ indent += ' ';
+ }
+
+ return indent + str.split('\n').join('\n' + indent);
+};
+
+module.exports = new Cucumber();
View
10 lib/kyuri/steps.js
@@ -23,6 +23,14 @@ exports.When = function (pattern, topicGenerator) {
});
};
+exports.And = function (pattern, topicGenerator) {
+ steps.push({
+ operator: 'And',
+ pattern: pattern,
+ generator: topicGenerator
+ });
+};
+
exports.Then = function (pattern, callbackGenerator) {
steps.push({
operator: 'Then',
@@ -33,4 +41,4 @@ exports.Then = function (pattern, callbackGenerator) {
exports.export = function (module) {
module.exports = steps;
-};
+};
View
5 package.json
@@ -11,11 +11,14 @@
"dependencies": {
"underscore": "1.1.x",
"vows": "0.5.x",
- "eyes": "0.1.x"
+ "eyes": "0.1.x",
+ "colors": "0.5.x",
+ "lingo": "0.0.x"
},
"devDependencies": {
"vows": "0.5.x"
},
+ "bin": "./bin/kyuri",
"main": "./lib/kyuri",
"scripts": { "test": "vows --spec" },
"engines": { "node": ">= 0.4.0" }
View
4 test/generator-test.js
@@ -6,9 +6,7 @@
*
*/
-require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
-
-var kyuri = require('kyuri'),
+var kyuri = require('../../kyuri'),
fs = require('fs'),
path = require('path'),
vows = require('vows'),
View
4 test/lexer-test.js
@@ -6,9 +6,7 @@
*
*/
-require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
-
-var kyuri = require('kyuri'),
+var kyuri = require('../../kyuri'),
fs = require('fs'),
path = require('path'),
vows = require('vows'),
View
4 test/parser-test.js
@@ -6,9 +6,7 @@
*
*/
-require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
-
-var kyuri = require('kyuri'),
+var kyuri = require('../../kyuri'),
fs = require('fs'),
path = require('path'),
vows = require('vows'),
View
51 test/simple-lexer-test.js
@@ -6,12 +6,31 @@
*
*/
-require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
-
-var kyuri = require('kyuri'),
+var kyuri = require('../../kyuri'),
vows = require('vows'),
assert = require('assert'),
eyes = require('eyes');
+
+var exampleRowTopic = function (row, values) {
+ return {
+ topic: kyuri.tokens(row),
+ "should be the right kind of row": function(tokens) {
+ assert.equal(tokens[0][0], 'EXAMPLE_ROW');
+ },
+ "should create valid tokens": function(tokens) {
+ assert.instanceOf(tokens, Array);
+ assert.equal(tokens.length, 3);
+ },
+ "should create the right token values": function(tokens) {
+ var lexedValues = tokens[0][1];
+
+ assert.equal(values.length, lexedValues.length);
+ for (var i = 0; i < values.length; i++) {
+ assert.equal(lexedValues[i], values[i]);
+ }
+ }
+ };
+}
vows.describe('kyuri/lexer/simple').addBatch({
"When using the Kyuri lexer": {
@@ -28,6 +47,30 @@ vows.describe('kyuri/lexer/simple').addBatch({
assert.instanceOf(tokens, Array);
assert.equal(tokens.length, 4);
}
- }
+ },
+ "a tag token": {
+ topic: kyuri.tokens('@tag1'),
+ "should create a single token literal": function(tokens) {
+ assert.instanceOf(tokens, Array);
+ assert.equal(tokens.length, 3);
+ }
+ },
+ "two tags": {
+ topic: kyuri.tokens('@tag1 @tag2'),
+ "should create two tag tokens": function(tokens) {
+ assert.instanceOf(tokens, Array);
+ assert.equal(tokens.length, 4);
+ }
+ },
+ "a step with an '@' symbol": {
+ topic: kyuri.tokens('Given I have a user with email address "paul@done.com"'),
+ "should create a single token literal": function(tokens) {
+ assert.instanceOf(tokens, Array);
+ assert.equal(tokens.length, 4);
+ }
+ },
+ "a simple example row": exampleRowTopic('| title | element1 | element2 |', ['title', 'element1', 'element2']),
+ "an example row with spaces": exampleRowTopic('| title 1 | element 1 |', ['title 1', 'element 1']),
+ "an example row with blank elements": exampleRowTopic('| title | |', ['title', ''])
}
}).export(module);
View
4 test/step-generator-test.js
@@ -6,9 +6,7 @@
*
*/
-require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
-
-var kyuri = require('kyuri'),
+var kyuri = require('../../kyuri'),
fs = require('fs'),
path = require('path'),
vows = require('vows'),
View
4 test/vows-runner-test.js
@@ -6,9 +6,7 @@
*
*/
-require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
-
-var kyuri = require('kyuri'),
+var kyuri = require('../../kyuri'),
fs = require('fs'),
path = require('path'),
vows = require('vows'),
Something went wrong with that request. Please try again.