UNPKG

8.35 kBJavaScriptView Raw
1var os = require('os')
2var path = require('path')
3var fs = require('fs')
4var builder = require('xmlbuilder')
5var pathIsAbsolute = require('path-is-absolute')
6
7/* XML schemas supported by the reporter: 'xmlVersion' in karma.conf.js,
8 'XMLconfigValue' as variable here.
9 0 = "old", original XML format. For example, SonarQube versions prior to 6.2
10 1 = first amended version. Compatible with SonarQube starting from 6.2
11*/
12
13// concatenate test suite(s) and test description by default
14function defaultNameFormatter (browser, result) {
15 return result.suite.join(' ') + ' ' + result.description
16}
17
18var JUnitReporter = function (baseReporterDecorator, config, logger, helper, formatError) {
19 var log = logger.create('reporter.junit')
20 var reporterConfig = config.junitReporter || {}
21 // All reporterConfig.something are for reading flags from the Karma config file
22 var pkgName = reporterConfig.suite || ''
23 var outputDir = reporterConfig.outputDir
24 var outputFile = reporterConfig.outputFile
25 var useBrowserName = reporterConfig.useBrowserName
26 var nameFormatter = reporterConfig.nameFormatter || defaultNameFormatter
27 var classNameFormatter = reporterConfig.classNameFormatter
28 var properties = reporterConfig.properties
29 // The below two variables have to do with adding support for new SonarQube XML format
30 var XMLconfigValue = reporterConfig.xmlVersion
31 var NEWXML
32 // We need one global variable for the tag <file> to be visible to functions
33 var exposee
34 var suites = []
35 var pendingFileWritings = 0
36 var fileWritingFinished = function () {}
37 var allMessages = []
38
39 // The NEWXML is just sugar, a flag. Remove it when there are more than 2
40 // supported XML output formats.
41 if (!XMLconfigValue) {
42 XMLconfigValue = 0
43 NEWXML = false
44 } else {
45 // Slack behavior: "If defined, assume to be 1" since we have only two formats now
46 XMLconfigValue = 1
47 NEWXML = true
48 }
49
50 if (outputDir == null) {
51 outputDir = '.'
52 }
53
54 outputDir = helper.normalizeWinPath(path.resolve(config.basePath, outputDir)) + path.sep
55
56 if (typeof useBrowserName === 'undefined') {
57 useBrowserName = true
58 }
59
60 baseReporterDecorator(this)
61
62 this.adapters = [
63 function (msg) {
64 allMessages.push(msg)
65 }
66 ]
67
68 // Creates the outermost XML element: <unitTest>
69 var initializeXmlForBrowser = function (browser) {
70 var timestamp = (new Date()).toISOString().substr(0, 19)
71 var suite
72 if (NEWXML) {
73 suite = suites[browser.id] = builder.create('unitTest')
74 suite.att('version', '1')
75 exposee = suite.ele('file', {'path': 'fixedString'})
76 } else {
77 suite = suites[browser.id] = builder.create('testsuite')
78 suite.att('name', browser.name)
79 .att('package', pkgName)
80 .att('timestamp', timestamp)
81 .att('id', 0)
82 .att('hostname', os.hostname())
83 var propertiesElement = suite.ele('properties')
84 propertiesElement.ele('property', {name: 'browser.fullName', value: browser.fullName})
85
86 // add additional properties passed in through the config
87 for (var property in properties) {
88 if (properties.hasOwnProperty(property)) {
89 propertiesElement.ele('property', {name: property, value: properties[property]})
90 }
91 }
92 }
93 }
94
95 // This function takes care of writing the XML into a file
96 var writeXmlForBrowser = function (browser) {
97 // Define the file name using rules
98 var safeBrowserName = browser.name.replace(/ /g, '_')
99 var newOutputFile
100 if (outputFile && pathIsAbsolute(outputFile)) {
101 newOutputFile = outputFile
102 } else if (outputFile != null) {
103 var dir = useBrowserName ? path.join(outputDir, safeBrowserName)
104 : outputDir
105 newOutputFile = path.join(dir, outputFile)
106 } else if (useBrowserName) {
107 newOutputFile = path.join(outputDir, 'TESTS-' + safeBrowserName + '.xml')
108 } else {
109 newOutputFile = path.join(outputDir, 'TESTS.xml')
110 }
111
112 var xmlToOutput = suites[browser.id]
113
114 if (!xmlToOutput) {
115 return // don't die if browser didn't start
116 }
117
118 pendingFileWritings++
119 helper.mkdirIfNotExists(path.dirname(newOutputFile), function () {
120 fs.writeFile(newOutputFile, xmlToOutput.end({pretty: true}), function (err) {
121 if (err) {
122 log.warn('Cannot write JUnit xml\n\t' + err.message)
123 } else {
124 log.debug('JUnit results written to "%s".', newOutputFile)
125 }
126
127 if (!--pendingFileWritings) {
128 fileWritingFinished()
129 }
130 })
131 })
132 }
133
134 // Return a 'safe' name for test. This will be the name="..." content in XML.
135 var getClassName = function (browser, result) {
136 var name = ''
137 // configuration tells whether to use browser name at all
138 if (useBrowserName) {
139 name += browser.name
140 .replace(/ /g, '_')
141 .replace(/\./g, '_') + '.'
142 }
143 if (pkgName) {
144 name += '.'
145 }
146 if (result.suite && result.suite.length > 0) {
147 name += result.suite.join(' ')
148 }
149 return name
150 }
151
152 // "run_start" - a test run is beginning for all browsers
153 this.onRunStart = function (browsers) {
154 // TODO(vojta): remove once we don't care about Karma 0.10
155 browsers.forEach(initializeXmlForBrowser)
156 }
157
158 // "browser_start" - a test run is beginning in _this_ browser
159 this.onBrowserStart = function (browser) {
160 initializeXmlForBrowser(browser)
161 }
162
163 // "browser_complete" - a test run has completed in _this_ browser
164 // writes the XML to file and releases memory
165 this.onBrowserComplete = function (browser) {
166 var suite = suites[browser.id]
167 var result = browser.lastResult
168 if (!suite || !result) {
169 return // don't die if browser didn't start
170 }
171
172 if (!NEWXML) {
173 suite.att('tests', result.total ? result.total : 0)
174 suite.att('errors', result.disconnected || result.error ? 1 : 0)
175 suite.att('failures', result.failed ? result.failed : 0)
176 suite.att('time', (result.netTime || 0) / 1000)
177 suite.ele('system-out').dat(allMessages.join() + '\n')
178 suite.ele('system-err')
179 }
180
181 writeXmlForBrowser(browser)
182
183 // Release memory held by the test suite.
184 suites[browser.id] = null
185 }
186
187 // "run_complete" - a test run has completed on all browsers
188 this.onRunComplete = function () {
189 allMessages.length = 0
190 }
191
192 // --------------------------------------------
193 // | Producing XML for individual testCase |
194 // --------------------------------------------
195 this.specSuccess = this.specSkipped = this.specFailure = function (browser, result) {
196 var testsuite = suites[browser.id]
197 var validMilliTime
198 var spec
199
200 if (!testsuite) {
201 return
202 }
203
204 // New in the XSD schema: only name and duration. classname is obsoleted
205 if (NEWXML) {
206 if (!result.time || result.time === 0) {
207 validMilliTime = 1
208 } else {
209 validMilliTime = result.time
210 }
211 }
212
213 // create the tag for a new test case
214 /*
215 if (NEWXML) {
216 spec = testsuite.ele('testCase', {
217 name: nameFormatter(browser, result),
218 duration: validMilliTime })
219 }
220 */
221
222 if (NEWXML) {
223 spec = exposee.ele('testCase', {
224 name: nameFormatter(browser, result),
225 duration: validMilliTime })
226 } else {
227 // old XML format. Code as-was
228 spec = testsuite.ele('testcase', {
229 name: nameFormatter(browser, result),
230 time: ((result.time || 0) / 1000),
231 classname: (typeof classNameFormatter === 'function' ? classNameFormatter : getClassName)(browser, result)
232 })
233 }
234
235 if (result.skipped) {
236 spec.ele('skipped')
237 }
238
239 if (!result.success) {
240 result.log.forEach(function (err) {
241 if (!NEWXML) {
242 spec.ele('failure', {type: ''}, formatError(err))
243 } else {
244 // In new XML format, an obligatory 'message' attribute in failure
245 spec.ele('failure', {message: formatError(err)})
246 }
247 })
248 }
249 }
250
251 // wait for writing all the xml files, before exiting
252 this.onExit = function (done) {
253 if (pendingFileWritings) {
254 fileWritingFinished = done
255 } else {
256 done()
257 }
258 }
259}
260
261JUnitReporter.$inject = ['baseReporterDecorator', 'config', 'logger', 'helper', 'formatError']
262
263// PUBLISH DI MODULE
264module.exports = {
265 'reporter:junit': ['type', JUnitReporter]
266}