xunit.js 5.1 KB

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