UNPKG

10.7 kBJavaScriptView Raw
1/**
2 * Module dependencies.
3 */
4
5var Base = require('mocha').reporters.Base
6 , color = Base.color
7 , fs = require('fs')
8 , path = require('path')
9 , diff= require('diff')
10 , mkdirp = require('mkdirp')
11 , util = require('util')
12 , xml = require('xml');
13
14
15/**
16 * Save timer references to avoid Sinon interfering (see GH-237).
17 */
18
19var Date = global.Date;
20
21/**
22 * Save original console.log.
23 */
24var log = console.log.bind(console);
25
26/**
27 * Expose `Jenkins`.
28 */
29
30exports = module.exports = Jenkins;
31
32/**
33 * Initialize a new `Jenkins` test reporter.
34 *
35 * @param {Runner} runner
36 * @api public
37 */
38
39function Jenkins(runner, options) {
40 Base.call(this, runner);
41 var self = this;
42 var fd, currentSuite;
43 var jsonResults = {};
44
45 options = (options && options.reporterOptions) || {};
46
47 // Default options
48 options.junit_report_stack = process.env.JUNIT_REPORT_STACK || options.junit_report_stack;
49 options.junit_report_path = process.env.JUNIT_REPORT_PATH || options.junit_report_path;
50 options.junit_report_name = process.env.JUNIT_REPORT_NAME || options.junit_report_name;
51 options.junit_report_packages = process.env.JUNIT_REPORT_PACKAGES || options.junit_report_packages;
52 options.jenkins_reporter_enable_sonar = process.env.JENKINS_REPORTER_ENABLE_SONAR || options.jenkins_reporter_enable_sonar;
53 options.jenkins_reporter_test_dir = process.env.JENKINS_REPORTER_TEST_DIR || options.jenkins_reporter_test_dir || 'test';
54
55 function genSuiteReport() {
56 var testCount = currentSuite.failures+currentSuite.passes;
57 if (currentSuite.tests.length > testCount) {
58 // we have some skipped suites included
59 testCount = currentSuite.tests.length;
60 }
61 if (testCount === 0) {
62 // no tests, we can safely skip printing this suite
63 return;
64 }
65
66 if (options.screenshots) {
67 var imagestring = options.imagestring || htmlEscape(currentSuite.suite.fullTitle());
68 var imagetype = options.imagetype || 'png';
69 if (options.screenshots == 'loop') {
70 var screenshotIndex = 0;
71 var screenshots = [];
72 var screenshot = '';
73 var files = fs.readdirSync(options.junit_report_path).sort();
74 for(var i in files) {
75 if (files[i].indexOf(imagestring)>-1){
76 screenshots.push(files[i]);
77 }
78 }
79 }
80 }
81
82 var testSuite = {
83 'testsuite': [{
84 _attr: {
85 name: currentSuite.suite.fullTitle(),
86 tests: testCount,
87 errors: 0, /* not supported */
88 failures: currentSuite.failures,
89 skipped: testCount-currentSuite.failures-currentSuite.passes,
90 timestamp: currentSuite.start.toISOString().slice(0, -5),
91 time: currentSuite.duration/1000
92 }
93 }]
94 };
95
96 var tests = currentSuite.tests;
97
98 if (tests.length === 0 && currentSuite.failures > 0) {
99 // Get the runnable that failed, which is a beforeAll or beforeEach
100 tests = [currentSuite.suite.ctx.runnable()];
101 }
102
103 tests.forEach(function(test) {
104 var testCase = {
105 'testcase': [{
106 _attr: {
107 classname: getClassName(test, currentSuite.suite),
108 name: test.title,
109 time: 0.000
110 }
111 }]
112 };
113
114 if (test.duration) {
115 testCase.testcase[0]['_attr'].time = test.duration/1000;
116 }
117
118 if (test.state == "failed") {
119 testCase.testcase.push({
120 'failure': [
121 {_attr: {message: test.err.message || ''}},
122 unifiedDiff(test.err)
123 ]
124 });
125
126 //screenshot name is either pulled in sorted order from junit_report_path
127 //or set as suitename + classname + title, then written with Jenkins ATTACHMENT tag
128 if (options.screenshots) {
129 var screenshotDir = path.join(process.cwd(), options.junit_report_path);
130 if (options.screenshots == 'loop') {
131 screenshot = path.join(screenshotDir, screenshots[screenshotIndex]);
132 screenshotIndex++;
133 } else {
134 screenshot = path.join(screenshotDir, imagestring +
135 getClassName(test, currentSuite.suite) + test.title + "." + imagetype);
136 }
137
138 testCase.testcase.push({'system-out': ['[[ATTACHMENT|' + screenshot + ']]']})
139 }
140 } else if(test.state === undefined) {
141 testCase.testcase.push({skipped: {}});
142 }
143
144 if (test.logEntries && test.logEntries.length) {
145 var systemOut = '';
146 test.logEntries.forEach(function (entry) {
147 var outstr = util.format.apply(util, entry) + '\n';
148 systemOut += outstr;
149 });
150 testCase.testcase.push({'system-out': {_cdata: systemOut}});
151 }
152 testSuite.testsuite.push(testCase);
153 });
154
155 jsonResults.testsuites.push(testSuite);
156 }
157
158 function startSuite(suite) {
159 if (suite.tests.length > 0) {
160 currentSuite = {
161 suite: suite,
162 tests: [],
163 start: new Date,
164 failures: 0,
165 passes: 0
166 };
167 log();
168 log(" "+suite.fullTitle());
169 }
170 }
171
172 function endSuite() {
173 if (currentSuite != null) {
174 currentSuite.duration = new Date - currentSuite.start;
175 log();
176 log(' Suite duration: '+(currentSuite.duration/1000)+' s, Tests: '+currentSuite.tests.length);
177 try {
178 genSuiteReport();
179 } catch (err) { log(err) }
180 currentSuite = null;
181 }
182 }
183
184 function addTestToSuite(test) {
185 currentSuite.tests.push(test);
186 }
187
188 function indent() {
189 return " ";
190 }
191
192 function htmlEscape(str) {
193 return String(str)
194 .replace(/&/g, '&')
195 .replace(/"/g, '"')
196 .replace(/'/g, ''')
197 .replace(/</g, '&lt;')
198 .replace(/>/g, '&gt;');
199 }
200
201 function unifiedDiff(err) {
202 function escapeInvisibles(line) {
203 return line.replace(/\t/g, '<tab>')
204 .replace(/\r/g, '<CR>')
205 .replace(/\n/g, '<LF>\n');
206 }
207 function cleanUp(line) {
208 if (line.match(/\@\@/)) return null;
209 if (line.match(/\\ No newline/)) return null;
210 return escapeInvisibles(line);
211 }
212 function notBlank(line) {
213 return line != null;
214 }
215
216 var actual = err.actual,
217 expected = err.expected;
218
219 var lines, msg = '';
220
221 if (err.actual && err.expected) {
222 // make sure actual and expected are strings
223 if (!(typeof actual === 'string' || actual instanceof String)) {
224 actual = JSON.stringify(err.actual);
225 }
226
227 if (!(typeof expected === 'string' || expected instanceof String)) {
228 expected = JSON.stringify(err.expected);
229 }
230
231 var diffstr = diff.createPatch('string', actual, expected);
232 lines = diffstr.split('\n').splice(4);
233 msg += lines.map(cleanUp).filter(notBlank).join('\n');
234 }
235
236 if (options.junit_report_stack && err.stack) {
237 if (msg) msg += '\n';
238 lines = err.stack.split('\n').slice(1);
239 msg += lines.map(cleanUp).filter(notBlank).join('\n');
240 }
241
242 return msg;
243 }
244
245 function getRelativePath(test) {
246 var relativeTestDir = options.jenkins_reporter_test_dir,
247 absoluteTestDir = path.join(process.cwd(), relativeTestDir);
248 return path.relative(absoluteTestDir, test.file);
249 }
250
251 function getClassName(test, suite) {
252 if (options.jenkins_reporter_enable_sonar) {
253 // Inspired by https://github.com/pghalliday/mocha-sonar-reporter
254 var relativeFilePath = getRelativePath(test),
255 fileExt = path.extname(relativeFilePath);
256 return relativeFilePath.replace(new RegExp(fileExt+"$"), '');
257 }
258 if (options.junit_report_packages) {
259 var testPackage = getRelativePath(test).replace(/[^\/]*$/, ''),
260 delimiter = testPackage ? '.' : '';
261 return testPackage + delimiter + suite.fullTitle();
262 }
263 if (options.junit_report_name) {
264 return options.junit_report_name + '.' + suite.fullTitle();
265 }
266 return suite.fullTitle();
267 }
268
269 runner.on('start', function() {
270 var reportPath = options.junit_report_path;
271 var suitesName = options.junit_report_name || 'Mocha Tests';
272 if (reportPath) {
273 try {
274 if (fs.existsSync(reportPath)) {
275 var isDirectory = fs.statSync(reportPath).isDirectory();
276 if (isDirectory) reportPath = path.join(reportPath, new Date().getTime() + ".xml");
277 } else {
278 mkdirp.sync(path.dirname(reportPath));
279 }
280 fd = fs.openSync(reportPath, 'w');
281 } catch (err) {
282 // Not much we can do except logging this
283 log('WARNING: Could not open report file ' + reportPath + ', running without report');
284 }
285 }
286 jsonResults = {
287 'testsuites': [
288 {_attr: {name: suitesName}}
289 ]
290 };
291 });
292
293 runner.on('end', function() {
294 endSuite();
295 if (fd) {
296 fs.writeSync(fd, xml(jsonResults, {indent: ' '}));
297 fs.closeSync(fd);
298 }
299 self.epilogue.call(self);
300 });
301
302 runner.on('suite', function (suite) {
303 if (currentSuite) {
304 endSuite();
305 }
306 startSuite(suite);
307 });
308
309 runner.on('test', function (test) {
310 test.logEntries = [];
311 console.log = function () {
312 log.apply(this, arguments);
313 test.logEntries.push(Array.prototype.slice.call(arguments));
314 };
315 });
316
317 runner.on('test end', function(/*test*/) {
318 console.log = log;
319 });
320
321 runner.on('pending', function(test) {
322 var fmt = indent()
323 + color('checkmark', ' -')
324 + color('pending', ' %s');
325 log(fmt, test.title);
326 addTestToSuite(test);
327 });
328
329 runner.on('pass', function(test) {
330 currentSuite.passes++;
331 var fmt = indent()
332 + color('checkmark', ' '+Base.symbols.dot)
333 + color('pass', ' %s: ')
334 + color(test.speed, '%dms');
335 log(fmt, test.title, test.duration);
336 addTestToSuite(test);
337 });
338
339 runner.on('fail', function(test, err) {
340 if (currentSuite == undefined) {
341 // Failure occurred outside of a test suite.
342 startSuite({
343 tests: ["other"],
344 fullTitle: function() { return "Non-test failures"; }
345 });
346 var n = ++currentSuite.failures;
347 var fmt = indent()
348 + color('fail', ' %d) %s');
349 if (test == undefined) {
350 log(fmt, n, "unknown");
351 addTestToSuite({
352 title: "unknown",
353 file: process.cwd() + "/other.js",
354 state: 'failed',
355 err: err
356 });
357 } else {
358 log(fmt, n, test.title);
359 addTestToSuite(test);
360 }
361 endSuite();
362 return;
363 }
364
365 n = ++currentSuite.failures;
366 fmt = indent()
367 + color('fail', ' %d) %s');
368 log(fmt, n, test.title);
369 addTestToSuite(test);
370 });
371}
372
373Jenkins.prototype.__proto__ = Base.prototype;