gulp-karmaJunitPlugin.js 8.4 KB

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