1 |
|
2 |
|
3 |
|
4 |
|
5 | var 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 |
|
17 |
|
18 |
|
19 | var Date = global.Date;
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | var log = console.log.bind(console);
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | exports = module.exports = Jenkins;
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | function 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 |
|
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 |
|
59 | testCount = currentSuite.tests.length;
|
60 | }
|
61 | if (testCount === 0) {
|
62 |
|
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,
|
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 |
|
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 |
|
127 |
|
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, '<')
|
198 | .replace(/>/g, '>');
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
373 | Jenkins.prototype.__proto__ = Base.prototype;
|