diff --git a/automation-tests/lib/convert_results.js b/automation-tests/lib/convert_results.js new file mode 100755 index 0000000000000000000000000000000000000000..fb7a78929a4debba68afe4d1f179435891df190c --- /dev/null +++ b/automation-tests/lib/convert_results.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/* + * Converts html reports into nice, machine readable JSON + * Run: $ ./convert_result.js result/index.html + */ + +const fs = require('fs'), + path = require('path'), + jsonselect = require('JSONSelect'), + htmlparser = require('htmlparser'); + + +function main (args) { + var file = fs.readFile(path.resolve(args[2]), "utf8", function (err, html) { + if (err) throw err; + parseReport(html); + }); +} + +function parseReport (html) { + var report = {}; + var handler = new htmlparser.DefaultHandler(function(err, dom) { + if (err) { + console.error("Error: " + err); + } else { + var results = jsonselect.match(':has(:root > .attribs > .id:val("results")) .children :has(:root > .name:val("tr"))', dom); + + // remove header row + results.shift(); + + results.forEach(function (node, i, array) { + var url; + var result = node.children[1].attribs.class; + + // skip traceback rows + if (!result) return; + + try { + url = result === 'error' ? + findJobUrl(array[i+1].children[1].children[1].children) : + node.children[9].children[0].attribs.href; + } catch (e) { + url = ''; + } + + var name = node.children[5].children[0].data; + + report[name] = { + success: result === 'passed', + class: node.children[3].children[0].data, + duration: node.children[7].children[0].data, + url: url + }; + }); + } + }); + + var parser = new htmlparser.Parser(handler); + parser.parseComplete(html); + return report; +} + +// extract saucelab url from error report +function findJobUrl (children) { + var result; + children.forEach(function (node) { + var match = node.raw.match(/https:\/\/saucelabs.com\/jobs\/[a-f0-9]+/); + if (match) result = match[0]; + }); + return result; +} + +exports.parseReport = parseReport; + +if (process.argv[1] === __filename) main(process.argv); diff --git a/automation-tests/run_saucelabs b/automation-tests/run_saucelabs new file mode 100755 index 0000000000000000000000000000000000000000..68e34ebd9976613f6f2d9af643def86920845bab --- /dev/null +++ b/automation-tests/run_saucelabs @@ -0,0 +1,364 @@ +#!/usr/bin/env node + +var child_process = require('child_process'), + path = require('path'), + _ = require('underscore'), + fs = require('fs'), + glob = require('minimatch'), + temp = require('temp'), + which = require('which'), + parseReport = require('./lib/convert_results.js').parseReport, + events = require('events'), + util = require('util'), + mkdirp = require('mkdirp'); + +function runCmd(cmd, opts, cb) { + if (!cb) { + cb = opts; + opts = { cwd: path.dirname(__dirname) }; + } + var cp = child_process.exec(cmd, opts, function(err, stdout, stderr) { + cb(err, stdout, stderr); + }); +} + +function TestRunner() { + events.EventEmitter.call(this); +} + +util.inherits(TestRunner, events.EventEmitter); + +// path to automation_tests +const testPath = path.join(path.dirname(__dirname), "automation-tests"); + +// ephemeral files to hold various credentials desired by py.test gunk +const sauceYAMLPath = temp.path({suffix: '.yaml'}), + credentialsYAMLPath = temp.path({suffix: '.yaml'}); + +// python arguments common to all tests +var globalPythonArgs = { + "-m": "py.test", + "--credentials": credentialsYAMLPath, + "--saucelabs": sauceYAMLPath, + "--webqatimeout": 90, + "--destructive": null, + "-q": null, + '--capabilities': JSON.stringify({ "avoid-proxy":"true"}) +}; + +// python arguments specific to different test classes +var testSpecificPythonArgs = { + "123done": { + "--baseurl": "http://dev.123done.org" + }, + "browserid": { + "--baseurl": "http://dev.123done.org" + }, + "myfavoritebeer": { + "--baseurl": "http://dev.myfavoritebeer.org" + } +}; + +var browserSpecificPythonArgs = { + "linux_firefox_13": { + '--platform': 'LINUX', + '--browsername': 'firefox', + '--browserver': '13' + }, + "linux_opera_12": { + '--platform': 'LINUX', + '--browsername': 'opera', + '--browserver': '12' + }, + "osx_firefox_14": { + '--platform': 'MAC', + '--browsername':'firefox', + '--browserver':'14' + }, + "vista_chrome": { + '--platform':'VISTA', + '--browsername':'chrome' + }, + "vista_firefox_13": { + '--platform':'VISTA', + '--browsername':'firefox', + '--browserver':'13' + }, + "vista_ie_9": { + '--platform':'VISTA', + '--browsername':'internet explorer', + '--browserver':'9' + }, + "xp_ie_8": { + '--platform':'XP', + '--browsername': 'internet explorer', + '--browserver':'8' + } +}; + +function escape(val) { + return '"'+val.replace(/(["'$`\\])/g,'\\$1')+'"'; +}; + +// now write a yaml file with sauce creds +function writeSauceYAML() { + var envVars = { + 'PERSONA_SAUCE_USER': 'username', + 'PERSONA_SAUCE_PASSWORD': 'password', + 'PERSONA_SAUCE_APIKEY': 'api-key' + }; + + var fileContents = ""; + Object.keys(envVars).forEach(function(key) { + if (!process.env[key]) throw "missing sauce labs creds from environment"; + fileContents += envVars[key] + ": " + process.env[key] + "\n" + }); + fs.writeFileSync(sauceYAMLPath, fileContents); +} + +// now write a yaml file with sauce creds +function writeCredsYAML() { + var envVars = { + 'PERSONA_EMAIL': 'email', + 'PERSONA_PASSWORD': 'password' + }; + + var fileContents = "default:\n"; + Object.keys(envVars).forEach(function(key) { + if (!process.env[key]) throw "missing exisiting users creds from environment: " + key; + fileContents += " " + envVars[key] + ": " + process.env[key] + "\n" + }); + fs.writeFileSync(credentialsYAMLPath, fileContents); +} + +// setup python testing environment iff required, return path to python +TestRunner.prototype._setupPythonEnv = function(cb) { + var binPath = path.join(testPath, "bid_selenium", "bin"); + + function isSetup(lcb) { + var pathToPython = path.join(binPath, "python"); + fs.stat(pathToPython, function(err, r) { + if (!err && r.isFile()) lcb(null, { pathToPython: pathToPython }); + else lcb("not setup"); + }); + } + function findExecutable(names, cb) { + if (!names.length) return cb("not found"); + var n = names.shift(); + which(n, function(err, p) { + if (err) return findExecutable(names, cb); + cb(null, p); + }); + } + + isSetup(function(err, r) { + if (!err) return cb(null, r); + + // time to set it up! + findExecutable([ 'virtualenv-2.7', 'virtualenv2', 'virtualenv' ], function(err, virtualenv) { + if (err) return cb("cannot find virtualenv"); + runCmd(virtualenv + " " + path.join(testPath, "bid_selenium"), function(err, stdout) { + if (err) return cb("cannot run virtualenv: " + err); + runCmd(path.join(binPath, "pip") + " install -Ur requirements.txt", { cwd: testPath }, function(err, stdout) { + if (err) cb(err); + else isSetup(cb); + }); + }); + }); + }); +} + +TestRunner.prototype.run = function(opts, cb) { + var self = this; + var processesRunning = 0; + var testReports = []; + var overallStartTime = new Date(); + + // once all tests are complete, crawl through the data and write + // summary information + function addSummaryInfo(results) { + // XXX: calculcate browser and tests summary information + return { + duration: ((new Date() - overallStartTime) / 1000.0), + reports: results + }; + } + + opts = opts || {}; + + writeSauceYAML(); + writeCredsYAML(); + + this._setupPythonEnv(function(err, testEnvDetails) { + if (err) { + console.log("ERROR: couldn't setup python environment"); + return cb(err); + } + Object.keys(browserSpecificPythonArgs).forEach(function(browser) { + if (opts.browser && !glob(browser, opts.browser.toString())) return; + var browserArgs = browserSpecificPythonArgs[browser]; + + Object.keys(testSpecificPythonArgs).forEach(function(test) { + if (opts['test-group'] && !glob(test, (opts['test-group']).toString())) return; + + var htmlReportPath = temp.path({suffix: '.html'}); + var testArgs = testSpecificPythonArgs[test]; + + // build up the command line arguments + var cmdargsObj = {}; + _.extend(cmdargsObj, globalPythonArgs, testArgs, browserArgs, + { '--webqareport': htmlReportPath }); + var cmdargs = ""; + Object.keys(cmdargsObj).forEach(function(flag) { + var spc = " "; + if (flag.substr(2) === '--') spc = "="; + if (null === cmdargsObj[flag]) { + cmdargs += flag + " "; + } else { + cmdargs += flag + spc + escape(cmdargsObj[flag].toString()) + " "; + } + }); + if (opts.single) { + cmdargs += " " + path.relative(testPath, opts.single); + } else { + cmdargs += " " + test; + } + self.emit('started', { browser: browser, test: test }); + var startTime = new Date(); + processesRunning++; + runCmd(testEnvDetails.pathToPython + " " + cmdargs, { cwd: testPath }, function(err, stdout, stderr) { + var report = { + browser: browser, + test: test, + duration: ((new Date() - startTime) / 1000.0), + stdout: stdout, + stderr: stderr, + err: err, + passed: !err + }; + + try { + report.htmlReport = fs.readFileSync(htmlReportPath); + } catch(e) { } + + // extract key information from the html report and attach it to the report object + report.results = parseReport(report.htmlReport); + + self.emit('finished', report); + + testReports.push(report); + + // remove artifacts + fs.unlink(htmlReportPath); + + if (--processesRunning === 0) { + fs.unlink(sauceYAMLPath); + fs.unlink(credentialsYAMLPath); + if (cb) cb(null, addSummaryInfo(testReports)); + } + }); + }); + }); + }); +}; + +module.exports = TestRunner; + +// if we're invoked from the command line, do command liney things +if (process.argv[1] === __filename) { + process.on('uncaughtException', function(err) { + console.log("OH NOES", err); + process.exit(1); + }); + + var argv = require('optimist') + .usage('Run selenium tests via sauce labs.\nUsage: $0') + .alias('help', 'h') + .describe('help', 'display this usage message') + .alias('list-browsers', 'lb') + .describe('lb', 'list available browsers to test on') + .alias('browser', 'b') + .describe('browser', 'specify which browser to run tests on (globs supported)') + .alias('list-test-groups', 'lt') + .describe('list-test-groups', 'list available groups of tests that can be run') + .alias('test-group', 't') + .describe('test-group', 'specify which test groups to run (globs supported)') + .describe('single', 'run a single test within the specified test group') + .alias('single', 's') + .default('browser', "linux_firefox_13") // default to a specific browser + test group + .default("test-group", "123done"); // lest folks accidentally launch large #s of || jobs + + var args = argv.argv; + + if (args.h) { + argv.showHelp(); + process.exit(0); + } + + if (args.lb) { + console.log("available browsers:"); + console.log(" * " + Object.keys(browserSpecificPythonArgs).join("\n * ")); + process.exit(0); + } + + if (args.lt) { + console.log("available tests:"); + console.log(" * " + Object.keys(testSpecificPythonArgs).join("\n * ")); + process.exit(0); + } + + // nice file names + var startTime = new Date(); + function testFileName(browser, test) { + function pad(n){return n<10 ? '0'+n : n} + var d = startTime; + var name = "" + + d.getFullYear() + + "." + pad(d.getMonth()+1)+'.' + + pad(d.getDate())+'-' + + pad(d.getHours())+':' + + pad(d.getMinutes())+':' + + pad(d.getSeconds()); + name += "-" + browser + "-" + test; + return name; + } + + var tester = new TestRunner(); + tester.on('started', function(e) { + console.log("STARTED:", e.browser + "/" + e.test); + }); + + tester.on('finished', function(report) { + // save off the report files + var basename = testFileName(report.browser, report.test); + var resultsPath = path.join(testPath, "results", basename); + mkdirp.sync(path.join(testPath, "results")); + fs.writeFileSync(resultsPath + ".html", report.htmlReport); + if (report.stderr.length) fs.writeFileSync(resultsPath + ".stderr.txt", report.stderr); + if (report.stdout.length) fs.writeFileSync(resultsPath + ".stdout.txt", report.stdout); + + var what = report.passed ? "PASSED" : "FAILED"; + console.log(what + ": " + report.browser + "/" + report.test + + " - " + report.duration.toFixed(2) + "s (" + + path.relative(process.cwd, resultsPath + ".html") + ")"); + + if (!report.passed) { + Object.keys(report.results).forEach(function (resultName) { + var result = report.results[resultName]; + if (result.success) return; + console.log(" " + result.class + ": " + result.url); + }); + if (report.stderr.length) { + console.log(" ERRORS:\n > " + report.stderr.split("\n").join("\n > ")); + } + } + }); + tester.run(args, function(report) { + // We would like to summarize: + // 1. total test duration + // 2. number of browsers failing + // 3. number of tests failing + // XXX: after ALL tests complete output a short summary +// console.log(JSON.stringify(report, null, " ")); + }); +} diff --git a/package.json b/package.json index 97519eb6f52cd5f80825df93cbfa2a41134c5be2..a58c81dcc0e612b5a1e2d25e5d3dbdeb66e66908 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,10 @@ "vows": "0.5.13", "awsbox": "0.2.15", "irc": "0.3.3", - "jshint": "0.7.1" + "jshint": "0.7.1", + "minimatch": "0.2.6", + "which": "1.0.5", + "htmlparser": "1.7.6" }, "scripts": { "postinstall": "node ./scripts/generate_ephemeral_keys.js",