diff --git a/lib/guard/jasmine.rb b/lib/guard/jasmine.rb index b57d258..3586330 100644 --- a/lib/guard/jasmine.rb +++ b/lib/guard/jasmine.rb @@ -49,7 +49,10 @@ class Jasmine < Guard statements_threshold: 0, functions_threshold: 0, branches_threshold: 0, - lines_threshold: 0 + lines_threshold: 0, + junit: false, + junit_consolidate: true, + junit_save_path: '' } # Initialize Guard::Jasmine. diff --git a/lib/guard/jasmine/cli.rb b/lib/guard/jasmine/cli.rb index 995e74c..f84ac1f 100644 --- a/lib/guard/jasmine/cli.rb +++ b/lib/guard/jasmine/cli.rb @@ -137,6 +137,21 @@ class CLI < Thor default: 0, desc: 'Lines coverage threshold' + method_option :junit, + type: :boolean, + default: false + desc: 'Whether to save jasmine test results in JUnit-compatible xml files' + + method_option :junit_consolidate, + type: :boolean, + default: false + desc: 'Whether to save nested describes within the same xml file as their parent' + + method_option :junit_save_path, + type: :string, + default: '' + desc: 'The directory to save junit xml files into' + # Run the Guard::Jasmine::Runner with options from # the command line. # @@ -169,6 +184,9 @@ def spec(*paths) runner_options[:notification] = false runner_options[:hide_success] = true runner_options[:max_error_notify] = 0 + runner_options[:junit] = options.junit + runner_options[:junit_consolidate] = options.junit_consolidate + runner_options[:junit_save_path] = options.junit_save_path ::Guard::UI.options = ::Guard::UI.options.merge({ :template => ':message' }) diff --git a/lib/guard/jasmine/phantomjs/guard-jasmine.coffee b/lib/guard/jasmine/phantomjs/guard-jasmine.coffee index 1666e60..348c88a 100644 --- a/lib/guard/jasmine/phantomjs/guard-jasmine.coffee +++ b/lib/guard/jasmine/phantomjs/guard-jasmine.coffee @@ -10,6 +10,9 @@ options = focus: /true/i.test phantom.args[3] console: phantom.args[4] || 'failure' errors: phantom.args[5] || 'failure' + junit: /true/i.test phantom.args[6] + junit_consolidate: /true/i.test phantom.args[7] + junit_save_path: phantom.args[8] || '' # Create the web page. # @@ -20,6 +23,8 @@ page = require('webpage').create() currentSpecId = -1 logs = {} errors = {} +resultsKey = "__jr" + Math.ceil(Math.random() * 1000000) +fs = require("fs") # Catch JavaScript errors # @@ -47,18 +52,57 @@ page.onConsoleMessage = (msg, line, source) -> # Initialize the page before the JavaScript is run. # page.onInitialized = -> + overloadPageEvaluate(page) + setupWriteFileFunction(page, resultsKey, fs.separator) + page.injectJs 'lib/console.js' page.injectJs 'lib/reporter.js' + page.injectJs 'lib/junit_reporter.js' - page.evaluate -> + setupReporters = -> # Attach the console reporter when the document is ready. window.onload = -> window.onload = null window.resultReceived = false window.reporter = new ConsoleReporter() if window.jasmine + jasmine.getEnv().addReporter(new JUnitXmlReporter("%save_path%", "%consolidate%")) jasmine.getEnv().addReporter(window.reporter) + page.evaluate(setupReporters, {save_path: options.junit_save_path, consolidate: options.junit_consolidate}) + + +getXmlResults = (page, key) -> + getWindowObj = -> + window["%resultsObj%"] || {} + page.evaluate getWindowObj, {resultsObj: key} + +replaceFunctionPlaceholders= (fn, replacements) -> + if replacements && typeof replacements == 'object' + fn = fn.toString() + for p of replacements + if replacements.hasOwnProperty(p) + match = new RegExp("%" + p + "%", "g") + loop + fn = fn.replace(match, replacements[p]) + break unless fn.indexOf(match) != -1 + fn + +overloadPageEvaluate = (page) -> + page._evaluate = page.evaluate + page.evaluate = (fn, replacements) -> + page._evaluate(replaceFunctionPlaceholders(fn, replacements)) + page + +setupWriteFileFunction= (page,key, path_separator) -> + saveData = () -> + window["%resultsObj%"] = {} + window.fs_path_separator = "%fs_path_separator%" + window.__phantom_writeFile = (filename, text) -> + window["%resultsObj%"][filename] = text; + + page.evaluate saveData, {resultsObj: key, fs_path_separator: path_separator} + # Open web page and run the Jasmine test runner # page.open options.url, (status) -> @@ -70,7 +114,6 @@ page.open options.url, (status) -> else waitFor jasmineReady, jasmineAvailable, options.timeout, jasmineMissing - # Test if the jasmine has been loaded # jasmineReady = -> @@ -116,6 +159,12 @@ specsTimedout = -> console.log JSON.stringify({ error: 'Timeout for the Jasmine test results!' }) specsDone = -> + if options.junit == true + xml_results = getXmlResults(page, resultsKey) + for filename of xml_results + if xml_results.hasOwnProperty(filename) && (output = xml_results[filename]) && typeof(output) == 'string' + fs.write(filename, output, 'w') + phantom.exit() # Wait until the test condition is true or a timeout occurs. diff --git a/lib/guard/jasmine/phantomjs/guard-jasmine.js b/lib/guard/jasmine/phantomjs/guard-jasmine.js index 05debfc..b3cc35d 100644 --- a/lib/guard/jasmine/phantomjs/guard-jasmine.js +++ b/lib/guard/jasmine/phantomjs/guard-jasmine.js @@ -1,5 +1,6 @@ +// Generated by CoffeeScript 1.6.3 (function() { - var currentSpecId, errors, jasmineAvailable, jasmineMissing, jasmineReady, logs, options, page, specsDone, specsReady, specsTimedout, waitFor; + var currentSpecId, errors, fs, getXmlResults, jasmineAvailable, jasmineMissing, jasmineReady, logs, options, overloadPageEvaluate, page, replaceFunctionPlaceholders, resultsKey, setupWriteFileFunction, specsDone, specsReady, specsTimedout, waitFor; phantom.injectJs('lib/result.js'); @@ -9,7 +10,10 @@ specdoc: phantom.args[2] || 'failure', focus: /true/i.test(phantom.args[3]), console: phantom.args[4] || 'failure', - errors: phantom.args[5] || 'failure' + errors: phantom.args[5] || 'failure', + junit: /true/i.test(phantom.args[6]), + junit_consolidate: /true/i.test(phantom.args[7]), + junit_save_path: phantom.args[8] || '' }; page = require('webpage').create(); @@ -20,6 +24,10 @@ errors = {}; + resultsKey = "__jr" + Math.ceil(Math.random() * 1000000); + + fs = require("fs"); + page.onError = function(msg, trace) { if (currentSpecId) { errors[currentSpecId] || (errors[currentSpecId] = []); @@ -32,7 +40,6 @@ page.onConsoleMessage = function(msg, line, source) { var result; - if (/^RUNNER_END$/.test(msg)) { result = page.evaluate(function() { return window.reporter.runnerResult; @@ -50,17 +57,78 @@ }; page.onInitialized = function() { + var setupReporters; + overloadPageEvaluate(page); + setupWriteFileFunction(page, resultsKey, fs.separator); page.injectJs('lib/console.js'); page.injectJs('lib/reporter.js'); - return page.evaluate(function() { + page.injectJs('lib/junit_reporter.js'); + setupReporters = function() { return window.onload = function() { window.onload = null; window.resultReceived = false; window.reporter = new ConsoleReporter(); if (window.jasmine) { + jasmine.getEnv().addReporter(new JUnitXmlReporter("%save_path%", "%consolidate%")); return jasmine.getEnv().addReporter(window.reporter); } }; + }; + return page.evaluate(setupReporters, { + save_path: options.junit_save_path, + consolidate: options.junit_consolidate + }); + }; + + getXmlResults = function(page, key) { + var getWindowObj; + getWindowObj = function() { + return window["%resultsObj%"] || {}; + }; + return page.evaluate(getWindowObj, { + resultsObj: key + }); + }; + + replaceFunctionPlaceholders = function(fn, replacements) { + var match, p; + if (replacements && typeof replacements === 'object') { + fn = fn.toString(); + for (p in replacements) { + if (replacements.hasOwnProperty(p)) { + match = new RegExp("%" + p + "%", "g"); + while (true) { + fn = fn.replace(match, replacements[p]); + if (fn.indexOf(match) === -1) { + break; + } + } + } + } + } + return fn; + }; + + overloadPageEvaluate = function(page) { + page._evaluate = page.evaluate; + page.evaluate = function(fn, replacements) { + return page._evaluate(replaceFunctionPlaceholders(fn, replacements)); + }; + return page; + }; + + setupWriteFileFunction = function(page, key, path_separator) { + var saveData; + saveData = function() { + window["%resultsObj%"] = {}; + window.fs_path_separator = "%fs_path_separator%"; + return window.__phantom_writeFile = function(filename, text) { + return window["%resultsObj%"][filename] = text; + }; + }; + return page.evaluate(saveData, { + resultsObj: key, + fs_path_separator: path_separator }); }; @@ -88,10 +156,8 @@ jasmineMissing = function() { var error, text; - text = page.evaluate(function() { var _ref; - return (_ref = document.getElementsByTagName('body')[0]) != null ? _ref.innerText : void 0; }); if (text) { @@ -114,10 +180,8 @@ specsTimedout = function() { var error, text; - text = page.evaluate(function() { var _ref; - return (_ref = document.getElementsByTagName('body')[0]) != null ? _ref.innerText : void 0; }); if (text) { @@ -133,12 +197,20 @@ }; specsDone = function() { + var filename, output, xml_results; + if (options.junit === true) { + xml_results = getXmlResults(page, resultsKey); + for (filename in xml_results) { + if (xml_results.hasOwnProperty(filename) && (output = xml_results[filename]) && typeof output === 'string') { + fs.write(filename, output, 'w'); + } + } + } return phantom.exit(); }; waitFor = function(test, ready, timeout, timeoutFunction) { var condition, interval, start, wait; - if (timeout == null) { timeout = 10000; } diff --git a/lib/guard/jasmine/phantomjs/lib/console.js b/lib/guard/jasmine/phantomjs/lib/console.js index 34cc39b..97aea14 100644 --- a/lib/guard/jasmine/phantomjs/lib/console.js +++ b/lib/guard/jasmine/phantomjs/lib/console.js @@ -1,3 +1,4 @@ +// Generated by CoffeeScript 1.6.3 (function() { var Console, __slice = [].slice; @@ -5,35 +6,29 @@ Console = (function() { function Console(console) { var log; - log = console.log; console.log = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; return log.call(console, Console.format.apply(Console, args)); }; console.info = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; return log.call(console, "INFO: " + (Console.format.apply(Console, args))); }; console.warn = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; return log.call(console, "WARN: " + (Console.format.apply(Console, args))); }; console.error = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; return log.call(console, "ERROR: " + (Console.format.apply(Console, args))); }; console.debug = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; return log.call(console, "DEBUG: " + (Console.format.apply(Console, args))); }; @@ -44,7 +39,6 @@ Console.format = function() { var arg, args, result, _i, _len, _this = this; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; result = []; if (typeof args[0] === 'string' && /%[sdifo]/gi.test(args[0])) { @@ -62,7 +56,6 @@ Console.inspect = function(object, type) { var match, result; - switch (type) { case '%s': result = String(object); @@ -100,7 +93,6 @@ Console.pp = function(object, depth) { var key, result, type, value, _i, _len; - if (depth == null) { depth = 0; } diff --git a/lib/guard/jasmine/phantomjs/lib/junit_reporter.js b/lib/guard/jasmine/phantomjs/lib/junit_reporter.js new file mode 100644 index 0000000..0f8df05 --- /dev/null +++ b/lib/guard/jasmine/phantomjs/lib/junit_reporter.js @@ -0,0 +1,224 @@ +(function() { + function elapsed(startTime, endTime) { + return (endTime - startTime)/1000; + } + + function ISODateString(d) { + function pad(n) { return n < 10 ? '0'+n : n; } + + return d.getFullYear() + '-' + + pad(d.getMonth()+1) + '-' + + pad(d.getDate()) + 'T' + + pad(d.getHours()) + ':' + + pad(d.getMinutes()) + ':' + + pad(d.getSeconds()); + } + + function trim(str) { + return str.replace(/^\s+/, "" ).replace(/\s+$/, "" ); + } + + function escapeInvalidXmlChars(str) { + return str.replace(/\&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/\'/g, "'"); + } + + /** + * Generates JUnit XML for the given spec run. + * Allows the test results to be used in java based CI + * systems like CruiseControl and Hudson. + * + * @param {string} savePath where to save the files + * @param {boolean} consolidate whether to save nested describes within the + * same file as their parent; default: true + * @param {boolean} useDotNotation whether to separate suite names with + * dots rather than spaces (ie "Class.init" not + * "Class init"); default: true + */ + var JUnitXmlReporter = function(savePath, consolidate, useDotNotation) { + this.savePath = savePath || ''; + this.consolidate = consolidate === jasmine.undefined ? true : consolidate; + this.useDotNotation = useDotNotation === jasmine.undefined ? true : useDotNotation; + }; + JUnitXmlReporter.finished_at = null; // will be updated after all files have been written + + JUnitXmlReporter.prototype = { + reportSpecStarting: function(spec) { + spec.startTime = new Date(); + + if (!spec.suite.startTime) { + spec.suite.startTime = spec.startTime; + } + }, + + reportSpecResults: function(spec) { + var results = spec.results(); + spec.didFail = !results.passed(); + spec.duration = elapsed(spec.startTime, new Date()); + spec.output = ''; + if(results.skipped) { + spec.output = spec.output + ""; + } + + var failure = ""; + var failures = 0; + var resultItems = results.getItems(); + for (var i = 0; i < resultItems.length; i++) { + var result = resultItems[i]; + + if (result.type == 'expect' && result.passed && !result.passed()) { + failures += 1; + failure += ''; + failure += escapeInvalidXmlChars(result.trace.stack || result.message); + failure += ""; + } + } + if (failure) { + spec.output += failure; + } + spec.output += ""; + }, + + reportSuiteResults: function(suite) { + var results = suite.results(); + var specs = suite.specs(); + var specOutput = ""; + // for JUnit results, let's only include directly failed tests (not nested suites') + var failedCount = 0; + + suite.status = results.passed() ? 'Passed.' : 'Failed.'; + if (results.totalCount === 0) { // todo: change this to check results.skipped + suite.status = 'Skipped.'; + } + + // if a suite has no (active?) specs, reportSpecStarting is never called + // and thus the suite has no startTime -- account for that here + suite.startTime = suite.startTime || new Date(); + suite.duration = elapsed(suite.startTime, new Date()); + + for (var i = 0; i < specs.length; i++) { + failedCount += specs[i].didFail ? 1 : 0; + specOutput += "\n " + specs[i].output; + } + suite.output = '\n'; + suite.output += specOutput; + suite.output += "\n"; + }, + + reportRunnerResults: function(runner) { + var suites = runner.suites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + var fileName = 'TEST-' + this.getFullName(suite, true) + '.xml'; + var output = ''; + // if we are consolidating, only write out top-level suites + if (this.consolidate && suite.parentSuite) { + continue; + } + else if (this.consolidate) { + output += "\n"; + output += this.getNestedOutput(suite); + output += "\n"; + this.writeFile(this.savePath, fileName, output); + } + else { + output += suite.output; + this.writeFile(this.savePath, fileName, output); + } + } + // When all done, make it known on JUnitXmlReporter + JUnitXmlReporter.finished_at = (new Date()).getTime(); + }, + + getNestedOutput: function(suite) { + var output = suite.output; + for (var i = 0; i < suite.suites().length; i++) { + output += this.getNestedOutput(suite.suites()[i]); + } + return output; + }, + + writeFile: function(path, filename, text) { + function getQualifiedFilename(separator) { + if (path && path.substr(-1) !== separator && filename.substr(0) !== separator) { + path += separator; + } + return path + filename; + } + + // Rhino + try { + // turn filename into a qualified path + if (path) { + filename = getQualifiedFilename(java.lang.System.getProperty("file.separator")); + // create parent dir and ancestors if necessary + var file = java.io.File(filename); + var parentDir = file.getParentFile(); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + } + // finally write the file + var out = new java.io.BufferedWriter(new java.io.FileWriter(filename)); + out.write(text); + out.close(); + return; + } catch (e) {} + // PhantomJS, via a method injected by phantomjs-testrunner.js + try { + // turn filename into a qualified path + filename = getQualifiedFilename(window.fs_path_separator); + __phantom_writeFile(filename, text); + return; + } catch (f) {} + // Node.js + try { + var fs = require("fs"); + var nodejs_path = require("path"); + var fd = fs.openSync(nodejs_path.join(path, filename), "w"); + fs.writeSync(fd, text, 0); + fs.closeSync(fd); + return; + } catch (g) {} + }, + + getFullName: function(suite, isFilename) { + var fullName; + if (this.useDotNotation) { + fullName = suite.description; + for (var parentSuite = suite.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + fullName = parentSuite.description + '.' + fullName; + } + } + else { + fullName = suite.getFullName(); + } + + // Either remove or escape invalid XML characters + if (isFilename) { + return fullName.replace(/[^\w]/g, ""); + } + return escapeInvalidXmlChars(fullName); + }, + + log: function(str) { + var console = jasmine.getGlobal().console; + + if (console && console.log) { + console.log(str); + } + } + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = JUnitXmlReporter; + } else { + window.JUnitXmlReporter = JUnitXmlReporter; + } +}).call(this); diff --git a/lib/guard/jasmine/phantomjs/lib/reporter.js b/lib/guard/jasmine/phantomjs/lib/reporter.js index e18e225..6c0bdb0 100644 --- a/lib/guard/jasmine/phantomjs/lib/reporter.js +++ b/lib/guard/jasmine/phantomjs/lib/reporter.js @@ -1,3 +1,4 @@ +// Generated by CoffeeScript 1.6.3 (function() { var ConsoleReporter; @@ -26,7 +27,6 @@ ConsoleReporter.prototype.reportSpecResults = function(spec) { var messages, result, specResult, _base, _i, _len, _name, _ref; - if (!spec.results().skipped) { specResult = { id: spec.id, @@ -52,7 +52,6 @@ ConsoleReporter.prototype.reportSuiteResults = function(suite) { var parent, suiteResult, _base, _ref; - if (!suite.results().skipped) { suiteResult = { id: suite.id, @@ -78,7 +77,6 @@ ConsoleReporter.prototype.reportRunnerResults = function(runner) { var end, runtime; - runtime = (new Date().getTime() - this.startTime) / 1000; this.runnerResult['passed'] = runner.results().failedCount === 0; this.runnerResult['stats'] = { @@ -101,7 +99,6 @@ ConsoleReporter.prototype.addNestedSuites = function(suiteResult) { var suite, _i, _len, _ref, _results; - if (this.nestedSuiteResults[suiteResult.id]) { _ref = this.nestedSuiteResults[suiteResult.id]; _results = []; @@ -116,7 +113,6 @@ ConsoleReporter.prototype.removeEmptySuites = function(suiteResult) { var suite, suites, _i, _len, _ref; - suites = []; _ref = suiteResult.suites; for (_i = 0, _len = _ref.length; _i < _len; _i++) { diff --git a/lib/guard/jasmine/phantomjs/lib/result.js b/lib/guard/jasmine/phantomjs/lib/result.js index 1abdbb9..10decea 100644 --- a/lib/guard/jasmine/phantomjs/lib/result.js +++ b/lib/guard/jasmine/phantomjs/lib/result.js @@ -1,3 +1,4 @@ +// Generated by CoffeeScript 1.6.3 (function() { var Result; @@ -11,10 +12,8 @@ Result.prototype.addLogs = function(suite) { var id, s, spec; - suite.suites = (function() { var _i, _len, _ref, _results; - _ref = suite.suites; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -26,7 +25,6 @@ if (suite.specs) { suite.specs = (function() { var _i, _len, _ref, _results; - _ref = suite.specs; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -47,10 +45,8 @@ Result.prototype.addErrors = function(suite) { var id, s, spec; - suite.suites = (function() { var _i, _len, _ref, _results; - _ref = suite.suites; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -62,7 +58,6 @@ if (suite.specs) { suite.specs = (function() { var _i, _len, _ref, _results; - _ref = suite.specs; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -83,7 +78,6 @@ Result.prototype.addGlobalError = function(suite) { var b, err, errMsg, globalErrors, noSpecs, noSuites, _i, _len, _ref; - noSuites = !suite.suites || suite.suites.length === 0; noSpecs = !suite.specs || suite.specs.length === 0; globalErrors = this.errors[-1] && this.errors[-1].length !== 0; @@ -102,10 +96,8 @@ Result.prototype.cleanResult = function(suite) { var s, spec, _i, _len, _ref; - suite.suites = (function() { var _i, _len, _ref, _results; - _ref = suite.suites; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { diff --git a/lib/guard/jasmine/runner.rb b/lib/guard/jasmine/runner.rb index cdca6f8..e98a496 100644 --- a/lib/guard/jasmine/runner.rb +++ b/lib/guard/jasmine/runner.rb @@ -94,7 +94,19 @@ def response_status_for(results) def run_jasmine_spec(file, options) suite = jasmine_suite(file, options) Formatter.info("Run Jasmine suite at #{ suite }") - IO.popen("#{ phantomjs_command(options) } \"#{ suite }\" #{ options[:timeout] * 1000 } #{ options[:specdoc] } #{ options[:focus] } #{ options[:console] } #{ options[:errors] }", 'r:UTF-8') + + arguments = [ + options[:timeout] * 1000, + options[:specdoc], + options[:focus], + options[:console], + options[:errors], + options[:junit], + options[:junit_consolidate], + "\"#{options[:junit_save_path]}\"" + ] + + IO.popen("#{ phantomjs_command(options) } \"#{ suite }\" #{arguments.collect {|i| i.to_s}.join(" ")}", 'r:UTF-8') end # Get the PhantomJS binary and script to execute.