Skip to content

Commit

Permalink
add cli for specifying multiple formatters
Browse files Browse the repository at this point in the history
closes #425
resolves #90
resolves #91
  • Loading branch information
charlierudolph committed Oct 12, 2015
1 parent 9959ca9 commit e402399
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 133 deletions.
47 changes: 47 additions & 0 deletions features/multiple_formatters.feature
@@ -0,0 +1,47 @@
Feature: Multiple Formatters

Background:
Given a file named "features/a.feature" with:
"""
Feature: some feature
Scenario: I've declared one step which passes
Given This step is passing
"""
And a file named "features/step_definitions/cucumber_steps.js" with:
"""
var cucumberSteps = function() {
this.Given(/^This step is passing$/, function(callback) { callback(); });
};
module.exports = cucumberSteps;
"""

Scenario: Ability to specify multiple formatters
When I run cucumber.js with `-f progress -f pretty:pretty.txt`
Then it outputs this text:
"""
.
1 scenario (1 passed)
1 step (1 passed)
<duration-stat>
"""
And the file "pretty.txt" has the text:
"""
Feature: some feature
Scenario: I've declared one step which passes # features/a.feature:3
Given This step is passing # features/a.feature:4
1 scenario (1 passed)
1 step (1 passed)
<duration-stat>
"""

Scenario: Invalid path
When I run cucumber.js with `-f progress -f pretty:invalid/pretty.txt`
Then the error output contains the text:
"""
ENOENT
"""
And the exit status should be non-zero
26 changes: 21 additions & 5 deletions features/step_definitions/cli_steps.js
Expand Up @@ -89,12 +89,13 @@ var cliSteps = function cliSteps() {
}
});

this.Then(/^the exit status should be ([0-9]+)$/, function (code, callback) {
this.Then(/^the exit status should be ([0-9]+|non-zero)$/, function (code, callback) {
var world = this;

var actualCode = world.lastRun.error ? world.lastRun.error.code : "0";
var actualCode = world.lastRun.error ? world.lastRun.error.code : 0;
var ok = (code === 'non-zero' && actualCode !== 0) || actualCode === parseInt(code);

if (actualCode != code) {
if (!ok) {
throw new Error("Exit code expected: \"" + code + "\"\n" +
"Got: \"" + actualCode + "\"\n" +
"Output:\n" + normalizeText(world.lastRun.stdout) + "\n" +
Expand Down Expand Up @@ -145,10 +146,10 @@ var cliSteps = function cliSteps() {
callback();
});

this.Then(/^the output contains the text:$/, function(expectedOutput, callback) {
this.Then(/^the (error )?output contains the text:$/, function(error, expectedOutput, callback) {
var world = this;

var actualOutput = world.lastRun['stdout'];
var actualOutput = error ? world.lastRun['stderr'] : world.lastRun['stdout'];

actualOutput = normalizeText(actualOutput);
expectedOutput = normalizeText(expectedOutput);
Expand All @@ -160,6 +161,21 @@ var cliSteps = function cliSteps() {
callback();
});

this.Then(/^the file "([^"]*)" has the text:$/, function (filePath, expectedContent, callback) {
var absoluteFilePath = tmpPath(filePath);
fs.readFile(absoluteFilePath, 'utf8', function (err, content){
if (err) { return callback(err); }

actualContent = normalizeText(content);
expectedContent = normalizeText(expectedContent);

if (actualContent != expectedContent)
throw new Error("Expected " + filePath + " to have content matching:\n'" + expectedContent + "'\n" +
"Got:\n'" + actualContent + "'.\n");
callback();
})
});

this.Then(/^I see the version of Cucumber$/, function(callback) {
var world = this;

Expand Down
11 changes: 7 additions & 4 deletions lib/cucumber/cli.js
Expand Up @@ -13,9 +13,11 @@ function Cli(argv) {
},

runSuiteWithConfiguration: function runSuiteWithConfiguration(configuration, callback) {
var runtime = Cucumber.Runtime(configuration);
var formatter = configuration.getFormatter();
runtime.attachListener(formatter);
var runtime = Cucumber.Runtime(configuration);
var formatters = configuration.getFormatters();
formatters.forEach(function (formatter) {
runtime.attachListener(formatter);
});
runtime.start(callback);
},

Expand Down Expand Up @@ -58,7 +60,8 @@ function Cli(argv) {
tags to exclude several tags you have to use\n\
logical AND: --tags ~@fixme --tags ~@buggy.\n\
\n\
-f, --format FORMAT How to format features (default: progress).\n\
-f, --format FORMAT[:PATH] How to format features (default: progress).\n\
Supply PATH to redirect that formatters output.\n\
Available formats:\n\
pretty : prints the feature as is\n\
progress: prints one character per scenario\n\
Expand Down
23 changes: 19 additions & 4 deletions lib/cucumber/cli/argument_parser.js
Expand Up @@ -4,6 +4,7 @@ function ArgumentParser(argv) {
var nopt = require('nopt');
var path = require('path');
var _ = require('underscore');
var fs = require('fs');
var options;

var self = {
Expand Down Expand Up @@ -106,9 +107,23 @@ function ArgumentParser(argv) {
return modules;
},

getFormat: function getFormat() {
var format = self.getOptionOrDefault(ArgumentParser.FORMAT_OPTION_NAME, ArgumentParser.DEFAULT_FORMAT_VALUE);
return format;
getFormats: function getFormats() {
var formats = self.getOptionOrDefault(ArgumentParser.FORMAT_OPTION_NAME, [ArgumentParser.DEFAULT_FORMAT_VALUE]);
var outputMapping = {};
formats.forEach(function (format) {
var parts = format.split(':');
var type = parts[0];
var outputTo = parts[1] || '';
outputMapping[outputTo] = type;
});
return _.map(outputMapping, function (type, outputTo) {
var stream = process.stdout;
if (outputTo) {
var fd = fs.openSync(outputTo, 'w');
stream = fs.createWriteStream(null, {fd: fd});
}
return {stream: stream, type: type};
});
},

getKnownOptionDefinitions: function getKnownOptionDefinitions() {
Expand All @@ -117,7 +132,7 @@ function ArgumentParser(argv) {
definitions[ArgumentParser.TAGS_OPTION_NAME] = [String, Array];
definitions[ArgumentParser.COMPILER_OPTION_NAME] = [String, Array];
definitions[ArgumentParser.PROFILE_OPTION_NAME] = [String, Array];
definitions[ArgumentParser.FORMAT_OPTION_NAME] = String;
definitions[ArgumentParser.FORMAT_OPTION_NAME] = [String, Array];
definitions[ArgumentParser.HELP_FLAG_NAME] = Boolean;
definitions[ArgumentParser.VERSION_FLAG_NAME] = Boolean;
definitions[ArgumentParser.COFFEE_SCRIPT_SNIPPETS_FLAG_NAME] = Boolean;
Expand Down
49 changes: 24 additions & 25 deletions lib/cucumber/cli/configuration.js
Expand Up @@ -5,31 +5,30 @@ function Configuration(argv) {
argumentParser.parse();

var self = {
getFormatter: function getFormatter() {
var formatter;
var format = argumentParser.getFormat();
var options = {
coffeeScriptSnippets: self.shouldSnippetsBeInCoffeeScript(),
snippets: self.shouldSnippetsBeShown(),
showSource: self.shouldShowSource()
};
switch(format) {
case Configuration.JSON_FORMAT_NAME:
formatter = Cucumber.Listener.JsonFormatter(options);
break;
case Configuration.PROGRESS_FORMAT_NAME:
formatter = Cucumber.Listener.ProgressFormatter(options);
break;
case Configuration.PRETTY_FORMAT_NAME:
formatter = Cucumber.Listener.PrettyFormatter(options);
break;
case Configuration.SUMMARY_FORMAT_NAME:
formatter = Cucumber.Listener.SummaryFormatter(options);
break;
default:
throw new Error('Unknown formatter name "' + format + '".');
}
return formatter;
getFormatters: function getFormatters() {
var formats = argumentParser.getFormats();
var formatters = formats.map(function (format) {
var options = {
coffeeScriptSnippets: self.shouldSnippetsBeInCoffeeScript(),
snippets: self.shouldSnippetsBeShown(),
showSource: self.shouldShowSource(),
stream: format.stream
};

switch(format.type) {
case Configuration.JSON_FORMAT_NAME:
return Cucumber.Listener.JsonFormatter(options);
case Configuration.PROGRESS_FORMAT_NAME:
return Cucumber.Listener.ProgressFormatter(options);
case Configuration.PRETTY_FORMAT_NAME:
return Cucumber.Listener.PrettyFormatter(options);
case Configuration.SUMMARY_FORMAT_NAME:
return Cucumber.Listener.SummaryFormatter(options);
default:
throw new Error('Unknown formatter name "' + format + '".');
}
});
return formatters;
},

getFeatureSources: function getFeatureSources() {
Expand Down
11 changes: 9 additions & 2 deletions lib/cucumber/listener/formatter.js
Expand Up @@ -12,12 +12,19 @@ function Formatter(options) {

self.log = function log(string) {
logs += string;
if (options.logToConsole)
process.stdout.write(string);
if (options.stream)
options.stream.write(string);
if (typeof(options.logToFunction) === 'function')
options.logToFunction(string);
};

self.finish = function finish(callback) {
if (options.stream && options.stream !== process.stdout)
options.stream.end(callback);
else
callback();
};

self.getLogs = function getLogs() {
return logs;
};
Expand Down
2 changes: 1 addition & 1 deletion lib/cucumber/listener/json_formatter.js
Expand Up @@ -175,7 +175,7 @@ function JsonFormatter(options) {
self.handleAfterFeaturesEvent = function handleAfterFeaturesEvent(event, callback) {
gherkinJsonFormatter.eof();
gherkinJsonFormatter.done();
callback();
self.finish(callback);
};

return self;
Expand Down
2 changes: 1 addition & 1 deletion lib/cucumber/listener/pretty_formatter.js
Expand Up @@ -139,7 +139,7 @@ function PrettyFormatter(options) {
self.handleAfterFeaturesEvent = function handleAfterFeaturesEvent(event, callback) {
var summaryLogs = summaryFormatter.getLogs();
self.log(summaryLogs);
callback();
self.finish(callback);
};

self.formatDataTable = function formatDataTable(stepResult, dataTable) {
Expand Down
2 changes: 1 addition & 1 deletion lib/cucumber/listener/progress_formatter.js
Expand Up @@ -58,7 +58,7 @@ function ProgressFormatter(options) {
var summaryLogs = summaryFormatter.getLogs();
self.log('\n\n');
self.log(summaryLogs);
callback();
self.finish(callback);
};

return self;
Expand Down
2 changes: 1 addition & 1 deletion lib/cucumber/listener/summary_formatter.js
Expand Up @@ -48,7 +48,7 @@ function SummaryFormatter(options) {

self.handleAfterFeaturesEvent = function handleAfterFeaturesEvent(event, callback) {
self.logSummary();
callback();
self.finish(callback);
};

self.storeFailedStepResult = function storeFailedStepResult(failedStepResult) {
Expand Down
35 changes: 20 additions & 15 deletions spec/cucumber/cli/argument_parser_spec.js
Expand Up @@ -3,6 +3,7 @@ require('../../support/spec_helper');
describe("Cucumber.Cli.ArgumentParser", function () {
var Cucumber = requireLib('cucumber');
var path = require('path');
var fs = require('fs');
var nopt;

var argumentParser, argv, slicedArgv;
Expand Down Expand Up @@ -196,8 +197,8 @@ describe("Cucumber.Cli.ArgumentParser", function () {
expect(knownOptionDefinitions['compiler']).toEqual([String, Array]);
});

it("defines a --format option", function () {
expect(knownOptionDefinitions['format']).toEqual(String);
it("defines a repeatable --format option", function () {
expect(knownOptionDefinitions['format']).toEqual([String, Array]);
});

it("defines a --strict flag", function () {
Expand Down Expand Up @@ -451,21 +452,25 @@ describe("Cucumber.Cli.ArgumentParser", function () {
});
});

describe("getFormat()", function () {
var format;
describe("getFormats()", function () {
var formats, fd, stream;

beforeEach(function () {
format = createSpy("format");
spyOn(argumentParser, 'getOptionOrDefault').and.returnValue(format);
});

it("gets the format option value", function () {
argumentParser.getFormat();
expect(argumentParser.getOptionOrDefault).toHaveBeenCalledWith(Cucumber.Cli.ArgumentParser.FORMAT_OPTION_NAME, 'pretty');
});

it("returns the format", function () {
expect(argumentParser.getFormat()).toBe(format);
formats = ['progress', 'summary', 'pretty:path/to/file'];
fd = createSpy('fd');
stream = createSpy('stream');
spyOn(argumentParser, 'getOptionOrDefault').and.returnValue(formats);
spyOn(fs, 'openSync').and.returnValue(fd);
spyOn(fs, 'createWriteStream').and.returnValue(stream);
});

it("returns the formats", function () {
expect(argumentParser.getFormats()).toEqual([
{stream: process.stdout, type: 'summary'},
{stream: stream, type: 'pretty'},
]);
expect(fs.openSync).toHaveBeenCalledWith('path/to/file', 'w');
expect(fs.createWriteStream).toHaveBeenCalledWith(null, {fd: fd});
});
});

Expand Down

0 comments on commit e402399

Please sign in to comment.