296 lines
8.8 KiB
JavaScript
296 lines
8.8 KiB
JavaScript
module.exports = function popper({
|
|
tests = 'browserify test.js'
|
|
, farm = 'browserstack'
|
|
, notunnel = false
|
|
, runner = 'mocha'
|
|
, browsers = []
|
|
, globals = ''
|
|
, port = 1945
|
|
, watch = '.'
|
|
, opts = {}
|
|
, timeout
|
|
, ripple
|
|
} = {}){
|
|
|
|
// defaults
|
|
const wait = debounce(timeout = timeout || +env.POPPER_TIMEOUT || 20000)(quit)
|
|
, maxRetries = 3
|
|
|
|
ripple = (ripple || rijs)(extend({ dir, port })(opts))
|
|
resdir(ripple, dir)
|
|
browsers = browsers
|
|
.map(canonical(farm))
|
|
.filter(Boolean)
|
|
|
|
// define data resources
|
|
ripple('results', {}, { from })
|
|
ripple('totals' , {})
|
|
|
|
// watch files
|
|
if (!isCI && watch) {
|
|
log('watching', watch)
|
|
|
|
chokidar.watch(watch, {
|
|
ignored: [/^\.(.*)[^\/\\]/, /[\/\\]\./, /node_modules(.+)popper/]
|
|
, ignoreInitial: true
|
|
, usePolling: false
|
|
, depth: 5
|
|
})
|
|
.on('change', debounce(generate))
|
|
}
|
|
|
|
// icons
|
|
ripple(require('browser-icons'))
|
|
|
|
// limit dashboard resources
|
|
ripple.to = limit(ripple.to)
|
|
|
|
// proxy errors and register agent details
|
|
ripple.server.on('connected', connected)
|
|
|
|
// serve assets
|
|
ripple.server.express
|
|
.use(compression())
|
|
.use('/utilise.min.js', send(local('utilise', 'utilise.min.js')))
|
|
.use('/utilise.js' , send(local('utilise', 'utilise.js')))
|
|
.use('/mocha.css' , send(local('mocha', 'mocha.css')))
|
|
.use('/mocha.js' , send(local('mocha', 'mocha.js')))
|
|
.use('/chai.js' , send(local('chai', 'chai.js')))
|
|
.use('/dashboard/:id' , send(local(`./client/${runner}/logs.html`)))
|
|
.use('/dashboard' , send(local('./client/dashboard.html')))
|
|
.use('/' , serve(local('./client')))
|
|
.use('/' , index())
|
|
|
|
return generate()
|
|
, spawn()
|
|
, ripple
|
|
|
|
function index(){
|
|
const head = is.arr(globals) ? globals.join('\n') : globals
|
|
, html = file(local(`./client/${runner}/index.html`))
|
|
.replace('<!-- { extra scripts } -->', head || '')
|
|
|
|
return (req, res) => res.send(html)
|
|
}
|
|
|
|
function generate() {
|
|
log('generating tests')
|
|
|
|
const bundle = write(local('./client/tests.js'))
|
|
, stream = is.fn(tests)
|
|
? tests()
|
|
: run('sh', ['-c', tests], { stdio: 'pipe' })
|
|
|
|
if (stream.stderr)
|
|
stream.stderr.pipe(process.stderr)
|
|
|
|
;((stream.stdout || stream)
|
|
.on('end', debounce(500)(reload))
|
|
.pipe(bundle)
|
|
.flow || noop)()
|
|
}
|
|
|
|
function from(req){
|
|
return req.data.type == 'RERUN' ? reload(req.data.value)
|
|
: req.data.type == 'SAVE' ? save(req.socket.platform, req.data.value)
|
|
: false
|
|
}
|
|
|
|
function save(platform, result) {
|
|
const { uid } = platform
|
|
, results = ripple('results')
|
|
, retries = uid in results ? results[uid].retries : 0
|
|
|
|
log('received result from', uid)
|
|
result.platform = platform
|
|
result.retries = retries
|
|
update(uid, result)(ripple('results'))
|
|
totals()
|
|
ci(result)
|
|
}
|
|
|
|
function ci(r) {
|
|
if (!isCI || r.stats.running) return
|
|
|
|
const browser = browsers
|
|
.filter(d => {
|
|
if (d._name && d._name !== r.platform.name) return false
|
|
if (d._version && d._version !== r.platform.version) return false
|
|
if (d._os && d._os !== r.platform.os.name) return false
|
|
if (d._os_version && d._os_version !== r.platform.os.version) return false
|
|
return true
|
|
})
|
|
.pop()
|
|
|
|
if (!browser) return log('result not in matrix'.red, r.platform.uid)
|
|
|
|
browser.passed_by = r.platform.uid
|
|
browser.passed = !r.stats.failures
|
|
browser.passed
|
|
? log('browser passed:', r.platform.uid.green.bold)
|
|
: err('browser failed:', r.platform.uid.red.bold)
|
|
|
|
if (!browser.passed && r.retries < maxRetries)
|
|
return log('retrying'.yellow, r.platform.uid, ++r.retries, '/', str(maxRetries).grey)
|
|
, reload(r.platform.uid)
|
|
|
|
if (farms[farm].status)
|
|
farms[farm].status(browser, r.platform)
|
|
|
|
const target = browsers.length
|
|
, passed = browsers.filter(by('passed')).length
|
|
, finished = browsers.filter(by('passed_by')).length
|
|
|
|
log('ci targets', str(passed).green.bold, '/', str(target).grey)
|
|
|
|
target === passed ? time(3000, d => process.exit(0))
|
|
: target === finished ? time(3000, d => (!env.POPPER_TIMEOUT && process.exit(1)))
|
|
: wait()
|
|
}
|
|
|
|
function connected(socket){
|
|
socket.platform = parse(socket)
|
|
socket.type = socket.handshake.url == '/dashboard' ? 'dashboard' : 'agent'
|
|
log('connected', socket.platform.uid.green, socket.type.grey)
|
|
|
|
socket.on('global err', (message, url, linenumber) => err('Global error: ', socket.platform.uid.bold, message, url, linenumber))
|
|
|
|
if (debug)
|
|
socket.on('console', function(){ log(socket.platform.uid.bold, 'says:', '', arguments[0], to.arr(arguments[1]).map(str).join(' ')) })
|
|
}
|
|
|
|
function quit(){
|
|
log('no updates received for', timeout/1000, 'seconds. timing out..')
|
|
process.exit(1)
|
|
}
|
|
|
|
function reload(uid) {
|
|
const uids = uid ? [uid] : ripple.server.ws.sockets.map(d => d.platform.uid)
|
|
|
|
uids
|
|
.map(uid => update(`${uid}.stats.running`, true)(ripple('results')))
|
|
|
|
const agents = ripple.server.ws.sockets
|
|
.filter(not(by('handshake.url', '/dashboard')))
|
|
.filter(by('platform.uid', is.in(uids)))
|
|
.map(emitReload)
|
|
.length
|
|
|
|
log('reloading', str(agents).cyan, 'agents', uid || '')
|
|
}
|
|
|
|
function totals() {
|
|
const res = values(ripple('results'))
|
|
return ripple('totals', {
|
|
tests: str(res.map(key('stats.tests')).filter(Boolean).pop() || '?')
|
|
, browsers: str(res.length)
|
|
, passing: str(res.map(key('stats.failures')).filter(is(0)).length || '0')
|
|
})
|
|
}
|
|
|
|
function spawn(){
|
|
ripple.server.once('listening').then(() => {
|
|
log('running on port', ripple.server.http.address().port)
|
|
!notunnel && require('ngrok').connect(ripple.server.http.address().port, (e, url) => {
|
|
log('tunnelling', url && url.magenta)
|
|
return e ? err('error setting up reverse tunnel', e.stack)
|
|
: browsers.map(boot(farm)(url))
|
|
})
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
const { values, key, str, not, by, grep, lo, is, debounce, extend, falsy, send, file, noop, update, identity, time, includes } = require('utilise/pure')
|
|
, write = require('fs').createWriteStream
|
|
, run = require('child_process').spawn
|
|
, { stringify } = require('cryonic')
|
|
, { resolve } = require('path')
|
|
, compression = require('compression')
|
|
, browserify = require('browserify')
|
|
, platform = require('platform')
|
|
, chokidar = require('chokidar')
|
|
, express = require('express')
|
|
, resdir = require('rijs.resdir')
|
|
, serve = require('serve-static')
|
|
, farms = require('./farms')
|
|
, wd = require('wd')
|
|
, rijs = opts => require('rijs.npm')(require('rijs')(opts))
|
|
|
|
const log = require('utilise/log')('[popper]')
|
|
, err = require('utilise/err')('[popper]')
|
|
, old = grep(console, 'log', /^(?!.*\[ri\/)/)
|
|
, env = process.env
|
|
, dir = __dirname
|
|
, isCI = env.CI === 'true'
|
|
, debug = lo(env.NODE_ENV) == 'debug'
|
|
|
|
const heartbeat = vm => setInterval(d => vm.eval('', e => { if (e) console.error(e) }), 30000)
|
|
|
|
const canonical = farm => browser => is.str(browser)
|
|
? farms[farm].browsers[browser]
|
|
: browser
|
|
|
|
const local = (module, file) => {
|
|
const base = !file ? __dirname : require.resolve(module)
|
|
, read = !file ? module : '../'+file
|
|
return resolve(base, read)
|
|
}
|
|
|
|
const emitReload = socket => socket.send(stringify({ data: { exec: () => location.reload() }}))
|
|
|
|
const parse = socket => {
|
|
const ua = socket.handshake.headers['user-agent']
|
|
, p = platform.parse(ua)
|
|
, o = {
|
|
name: lo(p.name)
|
|
, version: major(p.version)
|
|
, os: {
|
|
name: lo(p.os.family.split(' ').shift())
|
|
, version: major(p.os.version, p.os.family)
|
|
}
|
|
}
|
|
|
|
if (o.os.name == 'os') o.os.name = 'osx'
|
|
if (o.name == 'chrome mobile') o.name = 'chrome'
|
|
if (o.name == 'microsoft edge') o.name = 'ie'
|
|
|
|
const uid = o.name
|
|
+ '-' + o.version
|
|
+ '-' + o.os.name
|
|
+ '-' + o.os.version
|
|
|
|
o.uid = uid
|
|
|
|
return o
|
|
}
|
|
|
|
const major = (v, f) =>
|
|
v ? v.split('.').shift()
|
|
: includes('xp')(lo(f)) ? 'xp'
|
|
: '?'
|
|
|
|
const limit = next => (req, socket) => {
|
|
return socket.handshake.url == '/dashboard' ? next(req, socket) : false
|
|
}
|
|
|
|
const boot = farm => url => opts => {
|
|
const { _name = '?', _version = '?', _os = '?' } = opts
|
|
, { connect, parse = identity } = farms[farm]
|
|
, id = `${_name.cyan} ${_version.cyan} on ${_os}`
|
|
, vm = opts.vm = connect(wd)
|
|
|
|
if (!vm) err('failed to connect to ' + farm), process.exit(1)
|
|
|
|
log(`booting up ${id}`)
|
|
|
|
vm.init(parse(opts), e => {
|
|
if (e) return err(e, id)
|
|
log('initialised', id)
|
|
vm.get(url, e => {
|
|
if (e) return err(e, id)
|
|
log('opened to test page', id.cyan)
|
|
heartbeat(vm)
|
|
})
|
|
})
|
|
} |