reporters/tap.js

'use strict';
/**
 * @module TAP
 */
/**
 * Module dependencies.
 */

var util = require('util');
var Base = require('./base');
var constants = require('../runner').constants;
var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN;
var EVENT_RUN_END = constants.EVENT_RUN_END;
var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
var EVENT_TEST_END = constants.EVENT_TEST_END;
var inherits = require('../utils').inherits;
var sprintf = util.format;

/**
 * Expose `TAP`.
 */

exports = module.exports = TAP;

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

  var self = this;
  var n = 1;

  var tapVersion = '12';
  if (options && options.reporterOptions) {
    if (options.reporterOptions.tapVersion) {
      tapVersion = options.reporterOptions.tapVersion.toString();
    }
  }

  this._producer = createProducer(tapVersion);

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

  runner.on(EVENT_TEST_END, function() {
    ++n;
  });

  runner.on(EVENT_TEST_PENDING, function(test) {
    self._producer.writePending(n, test);
  });

  runner.on(EVENT_TEST_PASS, function(test) {
    self._producer.writePass(n, test);
  });

  runner.on(EVENT_TEST_FAIL, function(test, err) {
    self._producer.writeFail(n, test, err);
  });

  runner.once(EVENT_RUN_END, function() {
    self._producer.writeEpilogue(runner.stats);
  });
}

/**
 * Inherit from `Base.prototype`.
 */
inherits(TAP, Base);

/**
 * Returns a TAP-safe title of `test`.
 *
 * @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);

TAP.description = 'TAP-compatible output';