From c23d2cda22cc371b0dc86cc9a990c1e6cbc05b43 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 12 Jul 2016 13:49:27 +0200 Subject: [PATCH] Add basic TestingBot support. Fixes #252 --- lib/TestingbotBrowser.js | 279 +++++++++++++++++++++++++++++++++++++++ lib/zuul.js | 13 ++ package.json | 3 +- 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 lib/TestingbotBrowser.js diff --git a/lib/TestingbotBrowser.js b/lib/TestingbotBrowser.js new file mode 100644 index 0000000..ff325fb --- /dev/null +++ b/lib/TestingbotBrowser.js @@ -0,0 +1,279 @@ +var wd = require('wd'); +var EventEmitter = require('events').EventEmitter; +var FirefoxProfile = require('firefox-profile'); +var debug = require('debug'); +var omit = require('lodash').omit; +var xtend = require('xtend'); +var testingbotApi = require('testingbot-api'); +var _ = require('lodash'); + + +var setup_test_instance = require('./setup'); + +function TestingbotBrowser(conf, opt) { + if (!(this instanceof TestingbotBrowser)) { + return new TestingbotBrowser(conf, opt); + } + + var self = this; + self._conf = conf; + self._opt = opt; + self.stats = { + passed: 0, + failed: 0 + }; + self.debug = debug('zuul:tb:' + conf.browser + ':' + conf.version); + self.debug('browser conf: %j', omit(conf, ['username', 'key'])); +} + +TestingbotBrowser.prototype.__proto__ = EventEmitter.prototype; + +TestingbotBrowser.prototype.toString = function() { + var self = this; + var conf = self._conf; + return '<' + conf.browser + ' ' + conf.version + ' on ' + conf.platform + '>'; +}; + +TestingbotBrowser.prototype.start = function() { + var self = this; + var conf = self._conf; + + self.stopped = false; + self.stats = { + passed: 0, + failed: 0 + }; + + self.debug('running'); + var browser = self.browser = wd.remote('hub.testingbot.com', 80, conf.username, conf.key); + + self.controller = setup_test_instance(self._opt, function(err, url) { + if (err) { + return self.shutdown(err); + } + + self.emit('init', conf); + + var init_conf = xtend({ + build: conf.build, + name: conf.name, + tags: conf.tags || [], + browserName: conf.browser, + version: conf.version, + platform: conf.platform + }, conf.capabilities); + + if (conf.firefox_profile) { + var fp = new FirefoxProfile(); + var extensions = conf.firefox_profile.extensions; + for (var preference in conf.firefox_profile) { + if (preference !== 'extensions') { + fp.setPreference(preference, conf.firefox_profile[preference]); + } + } + extensions = extensions ? extensions : []; + fp.addExtensions(extensions, function () { + fp.encoded(function(zippedProfile) { + init_conf.firefox_profile = zippedProfile; + init(); + }); + }); + } else { + init(); + } + + function init() { + self.debug('queuing'); + + browser.init(init_conf, function(err) { + if (err) { + if (err.data) { + err.message += ': ' + err.data.split('\n').slice(0, 1); + } + return self.shutdown(err); + } + + var reporter = new EventEmitter(); + + reporter.on('test_end', function(test) { + if (!test.passed) { + return self.stats.failed++; + } + self.stats.passed++; + }); + + reporter.on('done', function(results) { + clearTimeout(self.noOutputTimeout); + self.debug('done'); + var passed = results.passed; + var called = false; + var api = new TbApi({api_key: self._conf.username, api_secret: self._conf.key }); + api.updateTest('test[passed]=' + passed, browser.sessionId, function(err) { + if (called) { + return; + } + + called = true; + self.shutdown(); + + if (err) { + return; + // don't let this error fail us + } + }); + + reporter.removeAllListeners(); + }); + + self.debug('open %s', url); + self.emit('start', reporter); + + var timeout = false; + var get_timeout = setTimeout(function() { + self.debug('timed out waiting for open %s', url); + timeout = true; + self.shutdown(new Error('Timeout opening url after ' + Math.round(self._opt.browser_open_timeout/1000) + 's')); + }, self._opt.browser_open_timeout); + + browser.get(url, function(err) { + self.debug('browser opened url'); + + if (timeout) { + return; + } + + clearTimeout(get_timeout); + if (err) { + return self.shutdown(err); + } + + // no new output for 30s => error + watchOutput(); + + function watchOutput() { + if (self._opt.browser_output_timeout === -1) { + return; + } + + clearTimeout(self.noOutputTimeout); + + self.noOutputTimeout = setTimeout(function() { + self.shutdown(new Error('Did not receive any new output from browser for ' + Math.round(self._opt.browser_output_timeout/1000) + 's, shutting down')); + }, self._opt.browser_output_timeout); + } + + (function wait() { + if (self.stopped) { + return; + } + + self.debug('waiting for test results from %s', url); + // take the last 1000 log lines + // careful, the less you log lines, the slower your test + // result will be. The test could be finished in the browser + // but not in your console since it can take a lot + // of time to get a lot of results + var js = '(window.zuul_msg_bus ? window.zuul_msg_bus.splice(0, 1000) : []);' + browser.eval(js, function(err, res) { + if (err) { + self.debug('err: %s', err.message); + return self.shutdown(err); + } + + res = res || []; + //When testing with microsoft edge: + //Adds length property to array-like object if not defined to execute filter properly + if (res.length === undefined) { + res.length = Object.keys(res).length; + } + self.debug('res.length: %s', res.length); + + // if we received some data, reset the no output watch timeout + if (res.length > 0) { + watchOutput(); + } + + var has_done = false; + Array.prototype.filter.call(res, Boolean).forEach(function(msg) { + if (msg.type === 'done') { + has_done = true; + } + + reporter.emit(msg.type, msg); + }); + + if (has_done) { + self.debug('finished tests for %s', url); + return; + } + + self.debug('fetching more results'); + + // if we found results, let's not wait + // to get more + if (res.length > 0) { + process.nextTick(wait); + } else { + // otherwise, let's wait a little so that we do not + // spam testingbot + setTimeout(wait, 2000); + } + }); + })(); + }); + }); + } + }); +}; + +TestingbotBrowser.prototype.shutdown = function(err) { + var self = this; + + clearTimeout(self.noOutputTimeout); + + self.stopped = true; + + var finish_shutdown = function () { + self.debug('shutdown'); + + if (self.controller) { + self.controller.shutdown(); + } + + if (err) { + // prefix browser err message with browser version + err.message = self._conf.browser + '@' + self._conf.version + ': ' + err.message; + self.emit('error', err); + return; + } + + self.emit('done', self.stats); + self.removeAllListeners(); + } + + // make sure the browser shuts down before continuing + if (self.browser) { + self.debug('quitting browser'); + + var timeout = false; + var quit_timeout = setTimeout(function() { + self.debug('timed out waiting for browser to quit'); + timeout = true; + finish_shutdown(); + }, 10 * 1000); + + self.browser.quit(function(err) { + if (timeout) { + return; + } + + clearTimeout(quit_timeout); + finish_shutdown(); + }); + } + else { + finish_shutdown(); + } +}; + +module.exports = TestingbotBrowser; diff --git a/lib/zuul.js b/lib/zuul.js index 6b3977a..d883e55 100644 --- a/lib/zuul.js +++ b/lib/zuul.js @@ -8,6 +8,7 @@ var control_app = require('./control-app'); var frameworks = require('../frameworks'); var setup_test_instance = require('./setup'); var SauceBrowser = require('./SauceBrowser'); +var TestingbotBrowser = require('./TestingbotBrowser'); var PhantomBrowser = require('./PhantomBrowser'); var Electron = require('./Electron'); @@ -78,6 +79,18 @@ Zuul.prototype.browser = function(info) { platform: info.platform, capabilities: config.capabilities }, config)); + + self._browsers.push(TestingbotBrowser({ + name: config.name, + build: process.env.TRAVIS_BUILD_NUMBER, + firefox_profile: info.firefox_profile, + username: config.username, + key: config.key, + browser: info.name, + version: info.version, + platform: info.platform, + capabilities: config.capabilities + }, config)); }; Zuul.prototype.run = function(done) { diff --git a/package.json b/package.json index 3dcee15..a98fda1 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "wd": "0.3.11", "xtend": "2.1.2", "yamljs": "0.1.4", - "zuul-localtunnel": "1.1.0" + "zuul-localtunnel": "1.1.0", + "testingbot-api": "1.0.1" }, "devDependencies": { "after": "~0.8.1",