UNPKG

22.8 kBJavaScriptView Raw
1/* global __phantom_writeFile */
2(function(global) {
3 var UNDEFINED,
4 exportObject;
5
6 if (typeof module !== "undefined" && module.exports) {
7 exportObject = exports;
8 } else {
9 exportObject = global.jasmineReporters = global.jasmineReporters || {};
10 }
11
12 function trim(str) { return str.replace(/^\s+/, "" ).replace(/\s+$/, "" ); }
13 function elapsed(start, end) { return (end - start)/1000; }
14 function isFailed(obj) { return obj.status === "failed"; }
15 function isSkipped(obj) { return obj.status === "pending"; }
16 function isDisabled(obj) { return obj.status === "disabled"; }
17 function pad(n) { return n < 10 ? "0"+n : n; }
18 function extend(dupe, obj) { // performs a shallow copy of all props of `obj` onto `dupe`
19 for (var prop in obj) {
20 if (obj.hasOwnProperty(prop)) {
21 dupe[prop] = obj[prop];
22 }
23 }
24 return dupe;
25 }
26 function ISODateString(d) {
27 return d.getFullYear() + "-" +
28 pad(d.getMonth()+1) + "-" +
29 pad(d.getDate()) + "T" +
30 pad(d.getHours()) + ":" +
31 pad(d.getMinutes()) + ":" +
32 pad(d.getSeconds());
33 }
34 function escapeControlChars(str) {
35 // Remove control character from Jasmine default output
36 return str.replace(/[\x1b]/g, "");
37 }
38 function escapeInvalidXmlChars(str) {
39 var escaped = str.replace(/&/g, "&amp;")
40 .replace(/</g, "&lt;")
41 .replace(/>/g, "&gt;")
42 .replace(/"/g, "&quot;")
43 .replace(/'/g, "&apos;");
44 return escapeControlChars(escaped);
45 }
46 function getQualifiedFilename(path, filename, separator) {
47 if (path && path.substr(-1) !== separator && filename.substr(0) !== separator) {
48 path += separator;
49 }
50 return path + filename;
51 }
52 function log(str) {
53 var con = global.console || console;
54 if (con && con.log) {
55 con.log(str);
56 }
57 }
58 /** Hooks into either process.stdout (node) or console.log, if that is not
59 * available (see https://gist.github.com/pguillory/729616).
60 */
61 function hook_stdout(callback) {
62 var old_write;
63 var useProcess;
64 if(typeof(process)!=="undefined") {
65 old_write = process.stdout.write;
66 useProcess = true;
67 process.stdout.write = (function(write) {
68 return function(string, encoding, fd) {
69 write.apply(process.stdout, arguments);
70 callback(string, encoding, fd);
71 };
72 })(old_write);
73 }
74 else {
75 old_write = console.log.bind(console);
76 useProcess = false;
77 console.log = (function(write) {
78 return function(string) {
79 write.apply(string);
80 callback(string, "utf8");
81 };
82 })(old_write);
83 }
84 return function() {
85 if(useProcess) {
86 process.stdout.write = old_write;
87 }
88 else {
89 console.log = old_write;
90 }
91 };
92 }
93
94 /**
95 * A delegate for letting the consumer
96 * modify the suite name when it is used inside the junit report.
97 * This is useful when running a test suite against multiple capabilities
98 * because the report can have unique names for each combination of suite/spec
99 * and capability/test environment.
100 *
101 * @callback modifySuiteName
102 * @param {string} fullName
103 * @param {object} suite
104 */
105
106 /**
107 * A delegate for letting the consumer
108 * modify the report filename when it is used inside the junit report.
109 * This is useful when running a test suite against multiple capabilities
110 * because the report can have unique names for each combination of suite/spec
111 * and capability/test environment.
112 *
113 * @callback modifyReportFileName
114 * @param {string} suggestedName
115 * @param {object} suite
116 */
117
118 /**
119 * Generates JUnit XML for the given spec run. There are various options
120 * to control where the results are written, and the default values are
121 * set to create as few .xml files as possible. It is possible to save a
122 * single XML file, or an XML file for each top-level `describe`, or an
123 * XML file for each `describe` regardless of nesting.
124 *
125 * Usage:
126 *
127 * jasmine.getEnv().addReporter(new jasmineReporters.JUnitXmlReporter(options));
128 *
129 * @param {object} [options]
130 * @param {string} [savePath] directory to save the files (default: '')
131 * @param {boolean} [consolidateAll] whether to save all test results in a
132 * single file (default: true)
133 * NOTE: if true, {filePrefix} is treated as the full filename (excluding
134 * extension)
135 * @param {boolean} [consolidate] whether to save nested describes within the
136 * same file as their parent (default: true)
137 * NOTE: true does nothing if consolidateAll is also true.
138 * NOTE: false also sets consolidateAll to false.
139 * @param {boolean} [useDotNotation] whether to separate suite names with
140 * dots instead of spaces, ie "Class.init" not "Class init" (default: true)
141 * @param {boolean} [useFullTestName] whether to use the fully qualified Test
142 * name for the TestCase name attribute, ie "Suite Name Spec Name" not
143 * "Spec Name" (default: false)
144 * @param {string} [filePrefix] is the string value that is prepended to the
145 * xml output file (default: junitresults-)
146 * NOTE: if consolidateAll is true, the default is simply "junitresults" and
147 * this becomes the actual filename, ie "junitresults.xml"
148 * @param {string} [package] is the base package for all test suits that are
149 * handled by this report {default: none}
150 * @param {function} [modifySuiteName] a delegate for letting the consumer
151 * modify the suite name when it is used inside the junit report.
152 * This is useful when running a test suite against multiple capabilities
153 * because the report can have unique names for each combination of suite/spec
154 * and capability/test environment.
155 * @param {function} [modifyReportFileName] a delegate for letting the consumer
156 * modify the report filename.
157 * This is useful when running a test suite against multiple capabilities
158 * because the report can have unique names for each combination of suite/spec
159 * and capability/test environment.
160 * @param {string} [stylesheetPath] is the string value that specifies a path
161 * to an XSLT stylesheet to add to the junit XML file so that it can be
162 * opened directly in a browser. (default: none, no xml-stylesheet tag is added)
163 * @param {function} [suppressDisabled] if true, will not include `disabled=".."` in XML output
164 * @param {function} [systemOut] a delegate for letting the consumer add content
165 * to a <system-out> tag as part of each <testcase> spec output. If provided,
166 * it is invoked with the spec object and the fully qualified suite as filename.
167 * @param {boolean} [captureStdout] enables capturing all output from stdout as spec output in the
168 * xml-output elements of the junit reports {default: false}. If a systemOut delegate is defined and captureStdout
169 * is true, the output of the spec can be accessed via spec._stdout
170 */
171 exportObject.JUnitXmlReporter = function(options) {
172 var self = this;
173 self.started = false;
174 self.finished = false;
175 // sanitize arguments
176 options = options || {};
177 self.savePath = options.savePath || "";
178 self.consolidate = options.consolidate === UNDEFINED ? true : options.consolidate;
179 self.consolidateAll = self.consolidate !== false && (options.consolidateAll === UNDEFINED ? true : options.consolidateAll);
180 self.useDotNotation = options.useDotNotation === UNDEFINED ? true : options.useDotNotation;
181 self.useFullTestName = options.useFullTestName === UNDEFINED ? false : options.useFullTestName;
182 if (self.consolidateAll) {
183 self.filePrefix = options.filePrefix || "junitresults";
184 } else {
185 self.filePrefix = typeof options.filePrefix === "string" ? options.filePrefix : "junitresults-";
186 }
187 self.package = typeof(options.package) === "string" ? escapeInvalidXmlChars(options.package) : UNDEFINED;
188 self.stylesheetPath = typeof(options.stylesheetPath) === "string" && options.stylesheetPath || UNDEFINED;
189
190 if(options.modifySuiteName && typeof options.modifySuiteName !== "function") {
191 throw new Error('option "modifySuiteName" must be a function');
192 }
193 if(options.modifyReportFileName && typeof options.modifyReportFileName !== "function") {
194 throw new Error('option "modifyReportFileName" must be a function');
195 }
196 if(options.systemOut && typeof options.systemOut !== "function") {
197 throw new Error('option "systemOut" must be a function');
198 }
199
200 self.captureStdout = options.captureStdout || false;
201 if(self.captureStdout && !options.systemOut) {
202 options.systemOut = function (spec) {
203 return spec._stdout;
204 };
205 }
206 self.removeStdoutWrapper = undefined;
207
208 var delegates = {};
209 delegates.modifySuiteName = options.modifySuiteName;
210 delegates.modifyReportFileName = options.modifyReportFileName;
211 delegates.systemOut = options.systemOut;
212
213 self.logEntries = [];
214
215 var suites = [],
216 currentSuite = null,
217 // when use use fit, jasmine never calls suiteStarted / suiteDone, so make a fake one to use
218 fakeFocusedSuite = {
219 id: "focused",
220 description: "focused specs",
221 fullName: "focused specs"
222 };
223
224 var __suites = {}, __specs = {};
225 function getSuite(suite) {
226 __suites[suite.id] = extend(__suites[suite.id] || {}, suite);
227 return __suites[suite.id];
228 }
229 function getSpec(spec, suite) {
230 __specs[spec.id] = extend(__specs[spec.id] || {}, spec);
231 var ret = __specs[spec.id];
232 if (suite && !ret._suite) {
233 ret._suite = suite;
234 suite._specs.push(ret);
235 }
236 return ret;
237 }
238
239 self.jasmineStarted = function() {
240 exportObject.startTime = new Date();
241 self.started = true;
242 if(self.captureStdout) {
243 self.removeStdoutWrapper = hook_stdout(function(string) {
244 self.logEntries.push(string);
245 });
246 }
247 };
248 self.suiteStarted = function(suite) {
249 suite = getSuite(suite);
250 suite._startTime = new Date();
251 suite._specs = [];
252 suite._suites = [];
253 suite._failures = 0;
254 suite._skipped = 0;
255 suite._disabled = 0;
256 suite._parent = currentSuite;
257 if (!currentSuite) {
258 suites.push(suite);
259 } else {
260 currentSuite._suites.push(suite);
261 }
262 currentSuite = suite;
263 };
264 self.specStarted = function(spec) {
265 if (!currentSuite) {
266 // focused spec (fit) -- suiteStarted was never called
267 self.suiteStarted(fakeFocusedSuite);
268 }
269 spec = getSpec(spec, currentSuite);
270 spec._startTime = new Date();
271 spec._stdout = "";
272 };
273 self.specDone = function(spec) {
274 spec = getSpec(spec, currentSuite);
275 spec._endTime = new Date();
276 storeOutput(spec);
277 if (isSkipped(spec)) { spec._suite._skipped++; }
278 if (isDisabled(spec)) { spec._suite._disabled++; }
279 if (isFailed(spec)) { spec._suite._failures += spec.failedExpectations.length; }
280 };
281 self.suiteDone = function(suite) {
282 suite = getSuite(suite);
283 if (suite._parent === UNDEFINED) {
284 // disabled suite (xdescribe) -- suiteStarted was never called
285 self.suiteStarted(suite);
286 }
287 suite._endTime = new Date();
288 currentSuite = suite._parent;
289 };
290 self.jasmineDone = function() {
291 if (currentSuite) {
292 // focused spec (fit) -- suiteDone was never called
293 self.suiteDone(fakeFocusedSuite);
294 }
295 var output = "";
296 var testSuitesResults = { disabled: 0, failures: 0, tests: 0, time: 0 };
297 for (var i = 0; i < suites.length; i++) {
298 output += self.getOrWriteNestedOutput(suites[i]);
299 // retrieve nested suite data to include in the testsuites tag
300 var suiteResults = self.getNestedSuiteData(suites[i]);
301 for (var key in suiteResults) {
302 testSuitesResults[key] += suiteResults[key];
303 }
304 }
305 // if we have anything to write here, write out the consolidated file
306 if (output) {
307 wrapOutputAndWriteFile(self.filePrefix, output, testSuitesResults);
308 }
309 //log("Specs skipped but not reported (entire suite skipped or targeted to specific specs)", totalSpecsDefined - totalSpecsExecuted + totalSpecsDisabled);
310
311 self.finished = true;
312 // this is so phantomjs-testrunner.js can tell if we're done executing
313 exportObject.endTime = new Date();
314 if(self.removeStdoutWrapper) {
315 self.removeStdoutWrapper();
316 }
317 };
318
319 self.formatSuiteData = function(suite) {
320 return {
321 disabled: suite._disabled || 0,
322 failures: suite._failures || 0,
323 tests: suite._specs.length || 0,
324 time: (suite._endTime.getTime() - suite._startTime.getTime()) || 0
325 };
326 };
327
328 self.getNestedSuiteData = function (suite) {
329 var suiteResults = self.formatSuiteData(suite);
330 for (var i = 0; i < suite._suites.length; i++) {
331 var childSuiteResults = self.getNestedSuiteData(suite._suites[i]);
332 for (var key in suiteResults) {
333 suiteResults[key] += childSuiteResults[key];
334 }
335 }
336 return suiteResults;
337 };
338
339 self.getOrWriteNestedOutput = function(suite) {
340 var output = suiteAsXml(suite);
341 for (var i = 0; i < suite._suites.length; i++) {
342 output += self.getOrWriteNestedOutput(suite._suites[i]);
343 }
344 if (self.consolidateAll || self.consolidate && suite._parent) {
345 return output;
346 } else {
347 // if we aren't supposed to consolidate output, just write it now
348 wrapOutputAndWriteFile(generateFilename(suite), output, self.getNestedSuiteData(suite));
349 return "";
350 }
351 };
352
353 self.writeFile = function(filename, text) {
354 var errors = [];
355 var path = self.savePath;
356
357 function phantomWrite(path, filename, text) {
358 // turn filename into a qualified path
359 filename = getQualifiedFilename(path, filename, window.fs_path_separator);
360 // write via a method injected by phantomjs-testrunner.js
361 __phantom_writeFile(filename, text);
362 }
363
364 function nodeWrite(path, filename, text) {
365 var fs = require("fs");
366 var nodejs_path = require("path");
367 require("mkdirp").sync(path); // make sure the path exists
368 var filepath = nodejs_path.join(path, filename);
369 var xmlfile = fs.openSync(filepath, "w");
370 fs.writeSync(xmlfile, text, 0);
371 fs.closeSync(xmlfile);
372 return;
373 }
374 // Attempt writing with each possible environment.
375 // Track errors in case no write succeeds
376 try {
377 phantomWrite(path, filename, text);
378 return;
379 } catch (e) { errors.push(" PhantomJs attempt: " + e.message); }
380 try {
381 nodeWrite(path, filename, text);
382 return;
383 } catch (f) { errors.push(" NodeJS attempt: " + f.message); }
384
385 // If made it here, no write succeeded. Let user know.
386 log("Warning: writing junit report failed for '" + path + "', '" +
387 filename + "'. Reasons:\n" +
388 errors.join("\n")
389 );
390 };
391
392 /******** Helper functions with closure access for simplicity ********/
393 function generateFilename(suite) {
394 return self.filePrefix + getFullyQualifiedSuiteName(suite, true) + ".xml";
395 }
396
397 function getFullyQualifiedSuiteName(suite, isFilename) {
398 var fullName;
399 if (self.useDotNotation || isFilename) {
400 fullName = suite.description;
401 for (var parent = suite._parent; parent; parent = parent._parent) {
402 fullName = parent.description + "." + fullName;
403 }
404 } else {
405 fullName = suite.fullName;
406 }
407
408 // Either remove or escape invalid XML characters
409 if (isFilename) {
410 var fileName = "",
411 rFileChars = /[\w.]/,
412 chr;
413 while (fullName.length) {
414 chr = fullName[0];
415 fullName = fullName.substr(1);
416 if (rFileChars.test(chr)) {
417 fileName += chr;
418 }
419 }
420 if(delegates.modifyReportFileName) {
421 fileName = delegates.modifyReportFileName(fileName, suite);
422 }
423 return fileName;
424 } else {
425
426 if(delegates.modifySuiteName) {
427 fullName = delegates.modifySuiteName(fullName, suite);
428 }
429
430 return escapeInvalidXmlChars(fullName);
431 }
432 }
433
434 function suiteAsXml(suite) {
435 var xml = '\n <testsuite name="' + getFullyQualifiedSuiteName(suite) + '"';
436 xml += ' timestamp="' + ISODateString(suite._startTime) + '"';
437 xml += ' hostname="localhost"'; // many CI systems like Jenkins don't care about this, but junit spec says it is required
438 xml += ' time="' + elapsed(suite._startTime, suite._endTime) + '"';
439 xml += ' errors="0"';
440 xml += ' tests="' + suite._specs.length + '"';
441 xml += ' skipped="' + suite._skipped + '"';
442 if (!options.suppressDisabled) {
443 xml += ' disabled="' + suite._disabled + '"';
444 }
445 // Because of JUnit's flat structure, only include directly failed tests (not failures for nested suites)
446 xml += ' failures="' + suite._failures + '"';
447 if (self.package) {
448 xml += ' package="' + self.package + '"';
449 }
450 xml += ">";
451
452 for (var i = 0; i < suite._specs.length; i++) {
453 xml += specAsXml(suite._specs[i]);
454 }
455 xml += "\n </testsuite>";
456 return xml;
457 }
458 function specAsXml(spec) {
459 var testName = self.useFullTestName ? spec.fullName : spec.description;
460
461 var xml = '\n <testcase classname="' + getFullyQualifiedSuiteName(spec._suite) + '"';
462 xml += ' name="' + escapeInvalidXmlChars(testName) + '"';
463 xml += ' time="' + elapsed(spec._startTime, spec._endTime) + '"';
464
465 var testCaseBody = "";
466 if (isSkipped(spec) || isDisabled(spec)) {
467 if (spec.pendingReason) {
468 testCaseBody = '\n <skipped message="' + trim(escapeInvalidXmlChars(spec.pendingReason)) + '" />';
469 } else {
470 testCaseBody = "\n <skipped />";
471 }
472 } else if (isFailed(spec)) {
473 for (var i = 0, failure; i < spec.failedExpectations.length; i++) {
474 failure = spec.failedExpectations[i];
475 testCaseBody += '\n <failure type="' + (failure.matcherName || "exception") + '"';
476 testCaseBody += ' message="' + trim(escapeInvalidXmlChars(failure.message))+ '"';
477 testCaseBody += ">";
478 testCaseBody += "<![CDATA[" + trim(escapeControlChars(failure.stack || failure.message)) + "]]>";
479 testCaseBody += "\n </failure>";
480 }
481 }
482
483 if (testCaseBody || delegates.systemOut) {
484 xml += ">" + testCaseBody;
485 if (delegates.systemOut) {
486 xml += "\n <system-out>" + trim(escapeInvalidXmlChars(delegates.systemOut(spec, getFullyQualifiedSuiteName(spec._suite, true)))) + "</system-out>";
487 }
488 xml += "\n </testcase>";
489 } else {
490 xml += " />";
491 }
492 return xml;
493 }
494 function storeOutput(spec) {
495 if(self.captureStdout && !isSkipped(spec)) {
496 if(!isSkipped(spec) && !isDisabled(spec)) {
497 spec._stdout = self.logEntries.join("") + "\n";
498 }
499 self.logEntries.splice(0, self.logEntries.length);
500 }
501 }
502 function getPrefix(results) {
503 results = results ? results : {};
504 // To remove complexity and be more DRY about the silly preamble and <testsuites> element
505 var prefix = '<?xml version="1.0" encoding="UTF-8" ?>';
506 if (self.stylesheetPath) {
507 prefix += '\n<?xml-stylesheet type="text/xsl" href="' + self.stylesheetPath + '" ?>';
508 }
509 prefix += "\n<testsuites " + (options.suppressDisabled ? "" : 'disabled="' + results.disabled + '" ') + 'errors="0" failures="' + results.failures +
510 '" tests="' + results.tests + '" time="' + results.time/1000 + '">';
511 return prefix;
512 }
513 var suffix = "\n</testsuites>";
514 function wrapOutputAndWriteFile(filename, text, testSuitesResults) {
515 if (filename.substr(-4) !== ".xml") { filename += ".xml"; }
516 self.writeFile(filename, (getPrefix(testSuitesResults) + text + suffix));
517 }
518 };
519})(this);