Skip to content
This repository has been archived by the owner on Oct 10, 2021. It is now read-only.

add basic browserstack support #126

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions bin/zuul
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ program
.option('--local [port]', 'port for manual testing in a local browser')
.option('--tunnel', 'establish a tunnel for outside access. only used when --local is specified')
.option('--phantom', 'run tests in phantomjs. PhantomJS must be installed separately.')
.option('--browserstack', 'run tests in BrowserStack instead of Sauce Labs.')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if --platform <sauce | browserstack | phantom > wouldn't be a better flag

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the --platform flag. Unlikely confused with --browser-platform.

.option('--tunnel-host <host url>', 'specify a localtunnel server to use for forwarding')
.option('--sauce-connect [tunnel-identifier]', 'use saucelabs with sauce connect instead of localtunnel. Optionally specify the tunnel-identifier')
.option('--server <the server script>', 'specify a server script to be run')
Expand All @@ -39,6 +40,7 @@ var config = {
ui: program.ui,
tunnel: program.tunnel,
phantom: program.phantom,
selenium_runner: program.browserstack ? 'BrowserStack' : 'SauceLabs',
prj_dir: process.cwd(),
tunnel_host: program.tunnelHost,
sauce_connect: program.sauceConnect,
Expand Down Expand Up @@ -67,10 +69,23 @@ if(!process.stdout.isTTY){
});
}

// optional additional local config or from $HOME/.zuulrc
var local_config = find_nearest_file('.zuulrc') || path.join(osenv.home(), '.zuulrc');
if (fs.existsSync(local_config)) {
var zuulrc = yaml.parse(fs.readFileSync(local_config, 'utf-8'));
config = xtend(zuulrc, config);
config.tunnel_host = config.tunnel_host || zuulrc.tunnel_host;
}

config.username = {'SauceLabs': process.env.SAUCE_USERNAME || config.sauce_username,
'BrowserStack': process.env.BROWSERSTACK_USERNAME || config.browserstack_username}[config.selenium_runner]
config.key = {'SauceLabs': process.env.SAUCE_ACCESS_KEY || config.sauce_key,
'BrowserStack': process.env.BROWSERSTACK_ACCESS_KEY || config.browserstack_key}[config.selenium_runner]

if (program.listAvailableBrowsers) {
scout_browser(function(err, all_browsers) {
scout_browser(config, function(err, all_browsers) {
if (err) {
console.error('Unable to get available browsers for saucelabs'.red);
console.error('Unable to get available browsers for '+config.selenium_runner+''.red);
console.error(err.stack);
return process.exit(1);
}
Expand Down Expand Up @@ -106,20 +121,6 @@ if (program.browserName) {
config = xtend(config, { browsers: [{ name: program.browserName, version: program.browserVersion, platform: program.browserPlatform }] });
}

// optional additional local config or from $HOME/.zuulrc
var local_config = find_nearest_file('.zuulrc') || path.join(osenv.home(), '.zuulrc');
if (fs.existsSync(local_config)) {
var zuulrc = yaml.parse(fs.readFileSync(local_config, 'utf-8'));
config = xtend(zuulrc, config);
config.tunnel_host = config.tunnel_host || zuulrc.tunnel_host;
}

var sauce_username = process.env.SAUCE_USERNAME;
var sauce_key = process.env.SAUCE_ACCESS_KEY;

config.username = sauce_username || config.sauce_username;
config.key = sauce_key || config.sauce_key;

if (!config.ui) {
console.error('Error: `ui` must be configured in .zuul.yml or specified with the --ui flag');
return process.exit(1);
Expand Down Expand Up @@ -155,15 +156,15 @@ else if (config.phantom) {

if (!config.username || !config.key) {
console.error('Error:');
console.error('Zuul tried to run tests in saucelabs, however no saucelabs credentials were provided.');
console.error('Zuul tried to run tests in '+config.selenium_runner+', however no '+config.selenium_runner+' credentials were provided.');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use string formatting:

console.error('Zuul tried %s however no %s, config.selenium_runner, config.selenium_runner).

Makes it easier to grok the string.

console.error('See the zuul wiki (https://github.com/defunctzombie/zuul/wiki/Cloud-testing) for info on how to setup cloud testing.');
process.exit(1);
return;
}

scout_browser(function(err, all_browsers) {
scout_browser(config, function(err, all_browsers) {
if (err) {
console.error('Unable to get available browsers for saucelabs'.red);
console.error('Unable to get available browsers for '+config.selenium_runner+''.red);
console.error(err.stack);
return process.exit(1);
}
Expand Down
1 change: 0 additions & 1 deletion examples/quickstart/.zuul.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: quickstart
ui: mocha-qunit
browsers:
- name: firefox
platform: linux
version: 30..latest
- name: chrome
version: 30..beta
236 changes: 236 additions & 0 deletions lib/BrowserStackBrowser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
var wd = require('wd');
var EventEmitter = require('events').EventEmitter;
var FirefoxProfile = require('firefox-profile');
var debug = require('debug')('zuul:browserstackbrowser');
var xtend = require('xtend');

var setup_test_instance = require('./setup');

function BrowserStackBrowser(conf, opt) {
if (!(this instanceof BrowserStackBrowser)) {
return new BrowserStackBrowser(conf, opt);
}

var self = this;
self._conf = conf;
self._opt = opt;
self._opt.tunnel = !opt.sauce_connect; // TODO: BrowserStack local testing
self.stats = {
passed: 0,
failed: 0
};
}

BrowserStackBrowser.prototype.__proto__ = EventEmitter.prototype;

BrowserStackBrowser.prototype.toString = function() {
var self = this;
var conf = self._conf;
return '<' + conf.browser + ' ' + conf.version + ' on ' + conf.platform + '>';
};

BrowserStackBrowser.prototype.start = function() {
var self = this;
var conf = self._conf;

self.stopped = false;
self.stats = {
passed: 0,
failed: 0
};

debug('running %s %s %s', conf.browser, conf.version, conf.platform);
var browser = self.browser = wd.remote('hub.browserstack.com', 80, conf.username, conf.key);

browser.configureHttp({
timeout: undefined,
retries: 1,
retryDelay: 1000
});

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: 'ANY'
}, 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() {
debug('queuing %s %s %s', conf.browser, conf.version, conf.platform);

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) {
debug('done %s %s %s', conf.browser, conf.version, conf.platform);
var passed = results.passed;
var called = false;
browser.sauceJobStatus(passed, function(err) {
if (called) {
return;
}

called = true;
self.shutdown();

if (err) {
return;
// don't let this error fail us
}
});

reporter.removeAllListeners();
});

debug('open %s', url);
self.emit('start', reporter);

var timeout = false;
var get_timeout = setTimeout(function() {
debug('timed out waiting for open %s', url);
timeout = true;
self.shutdown(new Error('Timeout opening url'));
}, 60 * 1000);

browser.get(url, function(err) {
if (timeout) {
return;
}

clearTimeout(get_timeout);
if (err) {
return self.shutdown(err);
}

(function wait() {
if (self.stopped) {
return;
}

debug('waiting for test results from %s', url);
var js = '(window.zuul_msg_bus ? window.zuul_msg_bus.splice(0, 10) : []);'
browser.eval(js, function(err, res) {
if (err) {
debug('err: %s', err.message);
}

debug('res.length: %s', res.length);

if (err) {
return self.shutdown(err);
}

var has_done = false;
res = res || [];
res.filter(Boolean).forEach(function(msg) {
if (msg.type === 'done') {
has_done = true;
}

reporter.emit(msg.type, msg);
});

if (has_done) {
debug('finished tests for %s', url);
return;
}

debug('fetching more results');
setTimeout(wait, 1000);
});
})();
});
});
}
});
};

BrowserStackBrowser.prototype.shutdown = function(err) {
var self = this;

self.stopped = true;

finish_shutdown = function() {
debug('shutdown');

if (self.controller) {
try { self.controller.shutdown(); } catch (e) {}
}

if (err) {
self.emit('error', err);
return;
}

self.emit('done', self.stats);
self.removeAllListeners();
}

// make sure the browser shuts down before continuing
if (self.browser) {
debug('quitting browser');

var timeout = false;
var quit_timeout = setTimeout(function() {
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 = BrowserStackBrowser;
11 changes: 11 additions & 0 deletions lib/flatten_browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ function flatten(request, all_browsers) {
return a.version - b.version;
});

var beta_available = avail[avail.length - 1].version === 'beta';

// remove duplicate version entries
// because we are not interested in testing on all platforms
avail.reduce(function(prev, curr, idx, arr) {
Expand Down Expand Up @@ -68,6 +70,11 @@ function flatten(request, all_browsers) {
// or ##..latest
function process_version_str(version) {
version = String(version);

if (version == 'beta' && !beta_available) {
console.log('Couldnt find beta version, using latest.');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to console log here. Just throw or do nothing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Zuul skips those browsers in general. Consistency would be cool.

version = 'latest';
}
if (version === 'latest') {
return get_numeric_versions(avail).slice(-1).map(addProfile);
}
Expand All @@ -94,6 +101,10 @@ function flatten(request, all_browsers) {
start_idx = v_map.indexOf(start);
}

if (end == 'beta' && !beta_available) {
console.log('Couldnt find beta version, using latest.');
end = 'latest';
}
if (end === 'latest') {
end_idx = get_numeric_versions(avail).length - 1;
}
Expand Down
Loading