diff --git a/docs/jasmine-options.md b/docs/jasmine-options.md index e0387e8..f7fc016 100644 --- a/docs/jasmine-options.md +++ b/docs/jasmine-options.md @@ -62,6 +62,14 @@ Default: `{}` These options will be passed to your template as an 'options' hash so that you can provide settings to your template. +## options.junit +Type: `Object` +Default: `{}` + +Set `options.junit.path` to generate JUnit compatible XML from the task (for use in a CI system such as Jenkins). + +Set `options.junit.consolidate` to consolidate the generated XML files so that there is one file per top level suite. + # Flags Name: `build` diff --git a/tasks/jasmine.js b/tasks/jasmine.js index 20f3205..5dfd034 100644 --- a/tasks/jasmine.js +++ b/tasks/jasmine.js @@ -32,7 +32,8 @@ module.exports = function(grunt) { var runners = { default : __dirname + '/jasmine/templates/DefaultRunner.tmpl', - requirejs : __dirname + '/jasmine/templates/RequireJSRunner.tmpl' + requirejs : __dirname + '/jasmine/templates/RequireJSRunner.tmpl', + junit : __dirname + '/jasmine/templates/JUnit.tmpl' }; var runnerOptions = { @@ -53,7 +54,8 @@ module.exports = function(grunt) { host : '', template: 'default', templateOptions : {}, - phantomjs : {} + phantomjs : {}, + junit: {} }); grunt.util._.defaults(options.templateOptions, runnerOptions[options.template] || {}); @@ -143,13 +145,14 @@ module.exports = function(grunt) { grunt.event.emit.apply(grunt.event, args); }); - phantomjs.on('jasmine.writeFile',function(type,filename, xml){ - var dir = options[type] && options[type].output; - if (dir) { - grunt.file.mkdir(dir); - grunt.file.write(path.join(dir, filename), xml); - } - }); + // Not used? +// phantomjs.on('jasmine.writeFile',function(type,filename, xml){ +// var dir = options[type] && options[type].output; +// if (dir) { +// grunt.file.mkdir(dir); +// grunt.file.write(path.join(dir, filename), xml); +// } +// }); phantomjs.on('jasmine.reportRunnerStarting',function(suites) { @@ -203,6 +206,46 @@ module.exports = function(grunt) { status.skipped += skippedAssertions; }); + phantomjs.on('jasmine.reportJUnitResults',function(junitData){ + if (options.junit && options.junit.path) { + + if (options.junit.consolidate) { + + grunt.util._(junitData.consolidatedSuites).each( + function(suites) + { + grunt.file.copy(runners.junit, path.join(options.junit.path, 'TEST-' + suites[0].name.replace(/[^\w]/g, '') + '.xml'), { + process: function(src) { + return grunt.util._.template( + src, + { + testsuites: suites + } + ); + } + }); + } + ); + } else { + junitData.suites.forEach( + function(suiteData) + { + grunt.file.copy(runners.junit, path.join(options.junit.path, 'TEST-' + suiteData.name.replace(/[^\w]/g, '') + '.xml'), { + process: function(src) { + return grunt.util._.template( + src, + { + testsuites: [suiteData] + } + ); + } + }); + } + ); + } + } + }); + phantomjs.on('jasmine.done',function(elapsed){ phantomjs.halt(); status.duration = elapsed; @@ -218,7 +261,4 @@ module.exports = function(grunt) { }); } -}; - - - +}; \ No newline at end of file diff --git a/tasks/jasmine/reporters/JUnitDataReporter.js b/tasks/jasmine/reporters/JUnitDataReporter.js new file mode 100644 index 0000000..8ca899a --- /dev/null +++ b/tasks/jasmine/reporters/JUnitDataReporter.js @@ -0,0 +1,94 @@ +/*global jasmine */ +(function() +{ + 'use strict'; + + function getNestedSuiteName(suite) + { + var names = []; + while (suite) { + names.unshift(suite.description); + suite = suite.parentSuite; + } + return names.join(' '); + } + + function getTopLevelSuiteId(suite) + { + var id; + while (suite) { + id = suite.id; + suite = suite.parentSuite; + } + return id; + } + + jasmine.JUnitDataReporter = function() + { + + }; + + jasmine.JUnitDataReporter.prototype = { + reportRunnerStarting: function(runner) { + }, + reportRunnerResults: function(runner) { + var suitesById = {}, + suites = runner.suites().map( + function(suite) + { + var failures = 0, + data = { + topLevelSuiteId: getTopLevelSuiteId(suite), + name: getNestedSuiteName(suite), + time: suite.duration / 1000, + timestamp: suite.timestamp, + tests: suite.specs().length, + errors: 0, // TODO: These exist in the JUnit XML but not sure how they map to jasmine things + testcases: suite.specs().map( + function(spec) + { + var failureMessages = []; + if (spec.results().failedCount) { + failures ++; + spec.results().items_.forEach( + function(expectation) + { + if (!expectation.passed()) { + failureMessages.push(expectation.message); + } + } + ); + } + return { + assertions: spec.results().items_.length, + className: getNestedSuiteName(spec.suite), + name: spec.description, + time: spec.duration / 1000, + failureMessages: failureMessages + }; + } + ) + }; + data.failures = failures; + suitesById[suite.id] = data; + return data; + } + ); + console.log('Suites:', suites); + }, + reportSuiteResults: function(suite) { + suite.timestamp = new Date(); + suite.duration = suite.timestamp.getTime() - suite.specs()[0].startTime; + }, + reportSpecStarting: function(spec) { + spec.startTime = (new Date()).getTime(); + }, + reportSpecResults: function(spec) { + spec.duration = (new Date()).getTime() - spec.startTime; + }, + log: function(str) { + console.log(str); + } + }; + +}()); \ No newline at end of file diff --git a/tasks/jasmine/reporters/PhantomReporter.js b/tasks/jasmine/reporters/PhantomReporter.js index 1657aa2..a953f61 100644 --- a/tasks/jasmine/reporters/PhantomReporter.js +++ b/tasks/jasmine/reporters/PhantomReporter.js @@ -40,6 +40,7 @@ }; PhantomReporter.prototype.reportSpecStarting = function(spec) { + spec.startTime = (new Date()).getTime(); var message = { suite : { description : spec.suite.description @@ -84,14 +85,19 @@ var specIds = runner.specs().map(function(a){return a.id;}); var summary = this.resultsForSpecs(specIds); phantom.sendMessage('jasmine.reportRunnerResults',summary); + phantom.sendMessage('jasmine.reportJUnitResults', this.generateJUnitSummary_(runner)); phantom.sendMessage('jasmine.done.PhantomReporter'); }; PhantomReporter.prototype.reportSuiteResults = function(suite) { - phantom.sendMessage('jasmine.reportSuiteResults',{ - description : suite.description, - results : suite.results() - }); + if (suite.specs().length) { + suite.timestamp = new Date(); + suite.duration = suite.timestamp.getTime() - suite.specs()[0].startTime; + phantom.sendMessage('jasmine.reportSuiteResults',{ + description : suite.description, + results : suite.results() + }); + } }; function stringify(obj) { @@ -130,6 +136,7 @@ } PhantomReporter.prototype.reportSpecResults = function(spec) { + spec.duration = (new Date()).getTime() - spec.startTime; var _results = spec.results(); var results = { description : _results.description, @@ -189,5 +196,78 @@ }; }; + function getNestedSuiteName(suite) + { + var names = []; + while (suite) { + names.unshift(suite.description); + suite = suite.parentSuite; + } + return names.join(' '); + } + + function getTopLevelSuiteId(suite) + { + var id; + while (suite) { + id = suite.id; + suite = suite.parentSuite; + } + return id; + } + + PhantomReporter.prototype.generateJUnitSummary_ = function(runner) { + var consolidatedSuites = {}, + suites = runner.suites().map( + function(suite) + { + var failures = 0, + data = { + name: getNestedSuiteName(suite), + time: suite.duration / 1000, + timestamp: suite.timestamp, + tests: suite.specs().length, + errors: 0, // TODO: These exist in the JUnit XML but not sure how they map to jasmine things + testcases: suite.specs().map( + function(spec) + { + var failureMessages = []; + if (spec.results().failedCount) { + failures ++; + spec.results().items_.forEach( + function(expectation) + { + if (!expectation.passed()) { + failureMessages.push(expectation.message); + } + } + ); + } + return { + assertions: spec.results().items_.length, + className: getNestedSuiteName(spec.suite), + name: spec.description, + time: spec.duration / 1000, + failureMessages: failureMessages + }; + } + ) + }; + data.failures = failures; + if (suite.parentSuite) { + consolidatedSuites[getTopLevelSuiteId(suite)].push(data); + } else { + consolidatedSuites[suite.id] = [data]; + } + return data; + } + ); + + return { + suites: suites, + consolidatedSuites: consolidatedSuites + }; + }; + jasmine.getEnv().addReporter( new PhantomReporter() ); }()); diff --git a/tasks/jasmine/templates/JUnit.tmpl b/tasks/jasmine/templates/JUnit.tmpl new file mode 100644 index 0000000..5f65516 --- /dev/null +++ b/tasks/jasmine/templates/JUnit.tmpl @@ -0,0 +1,14 @@ + + +<% testsuites.forEach(function(testsuite) { %> + + <% testsuite.testcases.forEach(function(testcase) { %> + + <% testcase.failureMessages.forEach(function(message) { %> + <%= message %> + <% }) %> + + <% }) %> + +<% }) %> + \ No newline at end of file