xunit.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. 'use strict';
  2. /**
  3. * @module XUnit
  4. */
  5. /**
  6. * Module dependencies.
  7. */
  8. var Base = require('./base');
  9. var utils = require('../utils');
  10. var fs = require('fs');
  11. var mkdirp = require('mkdirp');
  12. var path = require('path');
  13. var errors = require('../errors');
  14. var createUnsupportedError = errors.createUnsupportedError;
  15. var constants = require('../runner').constants;
  16. var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
  17. var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
  18. var EVENT_RUN_END = constants.EVENT_RUN_END;
  19. var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
  20. var STATE_FAILED = require('../runnable').constants.STATE_FAILED;
  21. var inherits = utils.inherits;
  22. var escape = utils.escape;
  23. /**
  24. * Save timer references to avoid Sinon interfering (see GH-237).
  25. */
  26. var Date = global.Date;
  27. /**
  28. * Expose `XUnit`.
  29. */
  30. exports = module.exports = XUnit;
  31. /**
  32. * Initialize a new `XUnit` reporter.
  33. *
  34. * @public
  35. * @class
  36. * @memberof Mocha.reporters
  37. * @extends Mocha.reporters.Base
  38. * @param {Runner} runner
  39. */
  40. function XUnit(runner, options) {
  41. Base.call(this, runner);
  42. var stats = this.stats;
  43. var tests = [];
  44. var self = this;
  45. // the name of the test suite, as it will appear in the resulting XML file
  46. var suiteName;
  47. // the default name of the test suite if none is provided
  48. var DEFAULT_SUITE_NAME = 'Mocha Tests';
  49. if (options && options.reporterOptions) {
  50. if (options.reporterOptions.output) {
  51. if (!fs.createWriteStream) {
  52. throw createUnsupportedError('file output not supported in browser');
  53. }
  54. mkdirp.sync(path.dirname(options.reporterOptions.output));
  55. self.fileStream = fs.createWriteStream(options.reporterOptions.output);
  56. }
  57. // get the suite name from the reporter options (if provided)
  58. suiteName = options.reporterOptions.suiteName;
  59. }
  60. // fall back to the default suite name
  61. suiteName = suiteName || DEFAULT_SUITE_NAME;
  62. runner.on(EVENT_TEST_PENDING, function(test) {
  63. tests.push(test);
  64. });
  65. runner.on(EVENT_TEST_PASS, function(test) {
  66. tests.push(test);
  67. });
  68. runner.on(EVENT_TEST_FAIL, function(test) {
  69. tests.push(test);
  70. });
  71. runner.once(EVENT_RUN_END, function() {
  72. self.write(
  73. tag(
  74. 'testsuite',
  75. {
  76. name: suiteName,
  77. tests: stats.tests,
  78. failures: 0,
  79. errors: stats.failures,
  80. skipped: stats.tests - stats.failures - stats.passes,
  81. timestamp: new Date().toUTCString(),
  82. time: stats.duration / 1000 || 0
  83. },
  84. false
  85. )
  86. );
  87. tests.forEach(function(t) {
  88. self.test(t);
  89. });
  90. self.write('</testsuite>');
  91. });
  92. }
  93. /**
  94. * Inherit from `Base.prototype`.
  95. */
  96. inherits(XUnit, Base);
  97. /**
  98. * Override done to close the stream (if it's a file).
  99. *
  100. * @param failures
  101. * @param {Function} fn
  102. */
  103. XUnit.prototype.done = function(failures, fn) {
  104. if (this.fileStream) {
  105. this.fileStream.end(function() {
  106. fn(failures);
  107. });
  108. } else {
  109. fn(failures);
  110. }
  111. };
  112. /**
  113. * Write out the given line.
  114. *
  115. * @param {string} line
  116. */
  117. XUnit.prototype.write = function(line) {
  118. if (this.fileStream) {
  119. this.fileStream.write(line + '\n');
  120. } else if (typeof process === 'object' && process.stdout) {
  121. process.stdout.write(line + '\n');
  122. } else {
  123. console.log(line);
  124. }
  125. };
  126. /**
  127. * Output tag for the given `test.`
  128. *
  129. * @param {Test} test
  130. */
  131. XUnit.prototype.test = function(test) {
  132. Base.useColors = false;
  133. var attrs = {
  134. classname: test.parent.fullTitle(),
  135. name: test.title,
  136. time: test.duration / 1000 || 0
  137. };
  138. if (test.state === STATE_FAILED) {
  139. var err = test.err;
  140. var diff =
  141. Base.hideDiff || !err.actual || !err.expected
  142. ? ''
  143. : '\n' + Base.generateDiff(err.actual, err.expected);
  144. this.write(
  145. tag(
  146. 'testcase',
  147. attrs,
  148. false,
  149. tag(
  150. 'failure',
  151. {},
  152. false,
  153. escape(err.message) + escape(diff) + '\n' + escape(err.stack)
  154. )
  155. )
  156. );
  157. } else if (test.isPending()) {
  158. this.write(tag('testcase', attrs, false, tag('skipped', {}, true)));
  159. } else {
  160. this.write(tag('testcase', attrs, true));
  161. }
  162. };
  163. /**
  164. * HTML tag helper.
  165. *
  166. * @param name
  167. * @param attrs
  168. * @param close
  169. * @param content
  170. * @return {string}
  171. */
  172. function tag(name, attrs, close, content) {
  173. var end = close ? '/>' : '>';
  174. var pairs = [];
  175. var tag;
  176. for (var key in attrs) {
  177. if (Object.prototype.hasOwnProperty.call(attrs, key)) {
  178. pairs.push(key + '="' + escape(attrs[key]) + '"');
  179. }
  180. }
  181. tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end;
  182. if (content) {
  183. tag += content + '</' + name + end;
  184. }
  185. return tag;
  186. }
  187. XUnit.description = 'XUnit-compatible XML output';