Skip to content
This repository has been archived by the owner on Dec 4, 2023. It is now read-only.

Commit

Permalink
Make TAP reporter TAP13-capable (mochajs#3552)
Browse files Browse the repository at this point in the history
* Refactored code to use `TAPProducer` objects to write output.
* Added TAP specification 13 output producer.
  * Version line
  * Errors and stacktraces inside YAML blocks
* Added TAP specification 12 output producer [default].
* Added `reporterOptions.tapVersion` to target TAP producer.
* Refactored to make use of `runner.stats`
* Refactored to use `process.stdout` stream exclusively.
* Updated to test against both specifications and incorporate work from PR mochajs#3528.
* Added integration tests for specification conformance.
  • Loading branch information
plroebuck committed Nov 10, 2018
1 parent cbc26c4 commit a271a2a
Show file tree
Hide file tree
Showing 4 changed files with 833 additions and 177 deletions.
264 changes: 235 additions & 29 deletions lib/reporters/tap.js
Expand Up @@ -6,7 +6,10 @@
* Module dependencies.
*/

var util = require('util');
var Base = require('./base');
var inherits = require('../utils').inherits;
var sprintf = util.format;

/**
* Expose `TAP`.
Expand All @@ -15,65 +18,268 @@ var Base = require('./base');
exports = module.exports = TAP;

/**
* Initialize a new `TAP` reporter.
* Constructs a new TAP reporter with runner instance and reporter options.
*
* @public
* @class
* @memberof Mocha.reporters
* @extends Mocha.reporters.Base
* @api public
* @param {Runner} runner
* @memberof Mocha.reporters
* @param {Runner} runner - Instance triggers reporter actions.
* @param {Object} [options] - runner options
*/
function TAP(runner) {
Base.call(this, runner);
function TAP(runner, options) {
Base.call(this, runner, options);

var self = this;
var n = 1;
var passes = 0;
var failures = 0;

runner.on('start', function() {
var total = runner.grepTotal(runner.suite);
console.log('%d..%d', 1, total);
var tapVersion = '12';
if (options && options.reporterOptions) {
if (options.reporterOptions.tapVersion) {
tapVersion = options.reporterOptions.tapVersion.toString();
}
}

this._producer = createProducer(tapVersion);

runner.once('start', function() {
var ntests = runner.grepTotal(runner.suite);
self._producer.writeVersion();
self._producer.writePlan(ntests);
});

runner.on('test end', function() {
++n;
});

runner.on('pending', function(test) {
console.log('ok %d %s # SKIP -', n, title(test));
self._producer.writePending(n, test);
});

runner.on('pass', function(test) {
passes++;
console.log('ok %d %s', n, title(test));
self._producer.writePass(n, test);
});

runner.on('fail', function(test, err) {
failures++;
console.log('not ok %d %s', n, title(test));
if (err.message) {
console.log(err.message.replace(/^/gm, ' '));
}
if (err.stack) {
console.log(err.stack.replace(/^/gm, ' '));
}
self._producer.writeFail(n, test, err);
});

runner.once('end', function() {
console.log('# tests ' + (passes + failures));
console.log('# pass ' + passes);
console.log('# fail ' + failures);
self._producer.writeEpilogue(runner.stats);
});
}

/**
* Return a TAP-safe title of `test`
* Inherit from `Base.prototype`.
*/
inherits(TAP, Base);

/**
* Returns a TAP-safe title of `test`.
*
* @api private
* @param {Object} test
* @return {String}
* @private
* @param {Test} test - Test instance.
* @return {String} title with any hash character removed
*/
function title(test) {
return test.fullTitle().replace(/#/g, '');
}

/**
* Writes newline-terminated formatted string to reporter output stream.
*
* @private
* @param {string} format - `printf`-like format string
* @param {...*} [varArgs] - Format string arguments
*/
function println(format, varArgs) {
var vargs = Array.from(arguments);
vargs[0] += '\n';
process.stdout.write(sprintf.apply(null, vargs));
}

/**
* Returns a `tapVersion`-appropriate TAP producer instance, if possible.
*
* @private
* @param {string} tapVersion - Version of TAP specification to produce.
* @returns {TAPProducer} specification-appropriate instance
* @throws {Error} if specification version has no associated producer.
*/
function createProducer(tapVersion) {
var producers = {
'12': new TAP12Producer(),
'13': new TAP13Producer()
};
var producer = producers[tapVersion];

if (!producer) {
throw new Error(
'invalid or unsupported TAP version: ' + JSON.stringify(tapVersion)
);
}

return producer;
}

/**
* @summary
* Constructs a new TAPProducer.
*
* @description
* <em>Only</em> to be used as an abstract base class.
*
* @private
* @constructor
*/
function TAPProducer() {}

/**
* Writes the TAP version to reporter output stream.
*
* @abstract
*/
TAPProducer.prototype.writeVersion = function() {};

/**
* Writes the plan to reporter output stream.
*
* @abstract
* @param {number} ntests - Number of tests that are planned to run.
*/
TAPProducer.prototype.writePlan = function(ntests) {
println('%d..%d', 1, ntests);
};

/**
* Writes that test passed to reporter output stream.
*
* @abstract
* @param {number} n - Index of test that passed.
* @param {Test} test - Instance containing test information.
*/
TAPProducer.prototype.writePass = function(n, test) {
println('ok %d %s', n, title(test));
};

/**
* Writes that test was skipped to reporter output stream.
*
* @abstract
* @param {number} n - Index of test that was skipped.
* @param {Test} test - Instance containing test information.
*/
TAPProducer.prototype.writePending = function(n, test) {
println('ok %d %s # SKIP -', n, title(test));
};

/**
* Writes that test failed to reporter output stream.
*
* @abstract
* @param {number} n - Index of test that failed.
* @param {Test} test - Instance containing test information.
* @param {Error} err - Reason the test failed.
*/
TAPProducer.prototype.writeFail = function(n, test, err) {
println('not ok %d %s', n, title(test));
};

/**
* Writes the summary epilogue to reporter output stream.
*
* @abstract
* @param {Object} stats - Object containing run statistics.
*/
TAPProducer.prototype.writeEpilogue = function(stats) {
// :TBD: Why is this not counting pending tests?
println('# tests ' + (stats.passes + stats.failures));
println('# pass ' + stats.passes);
// :TBD: Why are we not showing pending results?
println('# fail ' + stats.failures);
};

/**
* @summary
* Constructs a new TAP12Producer.
*
* @description
* Produces output conforming to the TAP12 specification.
*
* @private
* @constructor
* @extends TAPProducer
* @see {@link https://testanything.org/tap-specification.html|Specification}
*/
function TAP12Producer() {
/**
* Writes that test failed to reporter output stream, with error formatting.
* @override
*/
this.writeFail = function(n, test, err) {
TAPProducer.prototype.writeFail.call(this, n, test, err);
if (err.message) {
println(err.message.replace(/^/gm, ' '));
}
if (err.stack) {
println(err.stack.replace(/^/gm, ' '));
}
};
}

/**
* Inherit from `TAPProducer.prototype`.
*/
inherits(TAP12Producer, TAPProducer);

/**
* @summary
* Constructs a new TAP13Producer.
*
* @description
* Produces output conforming to the TAP13 specification.
*
* @private
* @constructor
* @extends TAPProducer
* @see {@link https://testanything.org/tap-version-13-specification.html|Specification}
*/
function TAP13Producer() {
/**
* Writes the TAP version to reporter output stream.
* @override
*/
this.writeVersion = function() {
println('TAP version 13');
};

/**
* Writes that test failed to reporter output stream, with error formatting.
* @override
*/
this.writeFail = function(n, test, err) {
TAPProducer.prototype.writeFail.call(this, n, test, err);
var emitYamlBlock = err.message != null || err.stack != null;
if (emitYamlBlock) {
println(indent(1) + '---');
if (err.message) {
println(indent(2) + 'message: |-');
println(err.message.replace(/^/gm, indent(3)));
}
if (err.stack) {
println(indent(2) + 'stack: |-');
println(err.stack.replace(/^/gm, indent(3)));
}
println(indent(1) + '...');
}
};

function indent(level) {
return Array(level + 1).join(' ');
}
}

/**
* Inherit from `TAPProducer.prototype`.
*/
inherits(TAP13Producer, TAPProducer);
63 changes: 63 additions & 0 deletions test/integration/fixtures/reporters.fixture.js
@@ -0,0 +1,63 @@
'use strict';

/**
* This file generates a wide range of output to test reporter functionality.
*/

describe('Animals', function() {

it('should consume organic material', function(done) { done(); });
it('should breathe oxygen', function(done) {
// we're a jellyfish
var actualBreathe = 'nothing';
var expectedBreathe = 'oxygen';
expect(actualBreathe, 'to equal', expectedBreathe);
done();
});
it('should be able to move', function(done) { done(); });
it('should reproduce sexually', function(done) { done(); });
it('should grow from a hollow sphere of cells', function(done) { done(); });

describe('Vertebrates', function() {
describe('Mammals', function() {
it('should give birth to live young', function(done) {
var expectedMammal = {
consumesMaterial: 'organic',
breathe: 'oxygen',
reproduction: {
type: 'sexually',
spawnType: 'live',
}
};
var platypus = JSON.parse(JSON.stringify(expectedMammal));
platypus['reproduction']['spawnType'] = 'hard-shelled egg';

expect(platypus, 'to equal', expectedMammal);
done();
});

describe('Blue Whale', function() {
it('should be the largest of all mammals', function(done) { done(); });
it('should have a body in some shade of blue', function(done) {
var bodyColor = 'blueish_grey';
var shadesOfBlue = ['cyan', 'light_blue', 'blue', 'indigo'];
expect(bodyColor, 'to be one of', shadesOfBlue);

done();
});
});
});
describe('Birds', function() {
it('should have feathers', function(done) { done(); });
it('should lay hard-shelled eggs', function(done) { done(); });
});
});

describe('Tardigrades', function() {
it('should answer to "water bear"', function(done) { done(); });
it('should be able to survive global mass extinction events', function(done) {
throw new Error("How do we even test for this without causing one?")
done();
});
});
});

0 comments on commit a271a2a

Please sign in to comment.