mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 01:38:07 +08:00
No changes in the changelog (https://github.com/karma-runner/karma/blob/master/CHANGELOG.md) to concern ourselves about, but one useful change is they upgraded the log4js dependency which was spitting out console messages on each test run. This is fixed with this version of Karma. Bug: none Change-Id: Ic49618cbda7a079cacb9813f5129692a83508a92 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3745621 Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
488 lines
16 KiB
JavaScript
488 lines
16 KiB
JavaScript
'use strict'
|
|
|
|
const SocketIO = require('socket.io')
|
|
const di = require('di')
|
|
const util = require('util')
|
|
const spawn = require('child_process').spawn
|
|
const tmp = require('tmp')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const NetUtils = require('./utils/net-utils')
|
|
const root = global || window || this
|
|
|
|
const cfg = require('./config')
|
|
const logger = require('./logger')
|
|
const constant = require('./constants')
|
|
const watcher = require('./watcher')
|
|
const plugin = require('./plugin')
|
|
|
|
const createServeFile = require('./web-server').createServeFile
|
|
const createServeStaticFile = require('./web-server').createServeStaticFile
|
|
const createFilesPromise = require('./web-server').createFilesPromise
|
|
const createWebServer = require('./web-server').createWebServer
|
|
const preprocessor = require('./preprocessor')
|
|
const Launcher = require('./launcher').Launcher
|
|
const FileList = require('./file-list')
|
|
const reporter = require('./reporter')
|
|
const helper = require('./helper')
|
|
const events = require('./events')
|
|
const KarmaEventEmitter = events.EventEmitter
|
|
const EventEmitter = require('events').EventEmitter
|
|
const Executor = require('./executor')
|
|
const Browser = require('./browser')
|
|
const BrowserCollection = require('./browser_collection')
|
|
const EmitterWrapper = require('./emitter_wrapper')
|
|
const processWrapper = new EmitterWrapper(process)
|
|
|
|
function createSocketIoServer (webServer, executor, config) {
|
|
const server = new SocketIO.Server(webServer, {
|
|
// avoid destroying http upgrades from socket.io to get proxied websockets working
|
|
destroyUpgrade: false,
|
|
path: config.urlRoot + 'socket.io/',
|
|
transports: config.transports,
|
|
forceJSONP: config.forceJSONP,
|
|
// Default is 5000 in socket.io v2.x and v3.x.
|
|
pingTimeout: config.pingTimeout || 5000,
|
|
// Default in v2 is 1e8 and coverage results can fail at 1e6
|
|
maxHttpBufferSize: 1e8
|
|
})
|
|
|
|
// hack to overcome circular dependency
|
|
executor.socketIoSockets = server.sockets
|
|
|
|
return server
|
|
}
|
|
|
|
class Server extends KarmaEventEmitter {
|
|
constructor (cliOptionsOrConfig, done) {
|
|
super()
|
|
cliOptionsOrConfig = cliOptionsOrConfig || {}
|
|
this.log = logger.create('karma-server')
|
|
done = helper.isFunction(done) ? done : process.exit
|
|
this.loadErrors = []
|
|
|
|
let config
|
|
if (cliOptionsOrConfig instanceof cfg.Config) {
|
|
config = cliOptionsOrConfig
|
|
} else {
|
|
logger.setupFromConfig({
|
|
colors: cliOptionsOrConfig.colors,
|
|
logLevel: cliOptionsOrConfig.logLevel
|
|
})
|
|
const deprecatedCliOptionsMessage =
|
|
'Passing raw CLI options to `new Server(config, done)` is ' +
|
|
'deprecated. Use ' +
|
|
'`parseConfig(configFilePath, cliOptions, {promiseConfig: true, throwErrors: true})` ' +
|
|
'to prepare a processed `Config` instance and pass that as the ' +
|
|
'`config` argument instead.'
|
|
this.log.warn(deprecatedCliOptionsMessage)
|
|
try {
|
|
config = cfg.parseConfig(
|
|
cliOptionsOrConfig.configFile,
|
|
cliOptionsOrConfig,
|
|
{
|
|
promiseConfig: false,
|
|
throwErrors: true
|
|
}
|
|
)
|
|
} catch (parseConfigError) {
|
|
// TODO: change how `done` falls back to exit in next major version
|
|
// SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378
|
|
done(1)
|
|
return
|
|
}
|
|
}
|
|
|
|
this.log.debug('Final config', util.inspect(config, false, /** depth **/ null))
|
|
|
|
if (!config.autoWatch && !config.singleRun) {
|
|
this.log.warn('`autowatch` and `singleRun` are both `false`. In order to execute tests use `karma run`.')
|
|
}
|
|
|
|
let modules = [{
|
|
helper: ['value', helper],
|
|
logger: ['value', logger],
|
|
done: ['value', done || process.exit],
|
|
emitter: ['value', this],
|
|
server: ['value', this],
|
|
watcher: ['value', watcher],
|
|
launcher: ['factory', Launcher.factory],
|
|
config: ['value', config],
|
|
instantiatePlugin: ['factory', plugin.createInstantiatePlugin],
|
|
preprocess: ['factory', preprocessor.createPriorityPreprocessor],
|
|
fileList: ['factory', FileList.factory],
|
|
webServer: ['factory', createWebServer],
|
|
serveFile: ['factory', createServeFile],
|
|
serveStaticFile: ['factory', createServeStaticFile],
|
|
filesPromise: ['factory', createFilesPromise],
|
|
socketServer: ['factory', createSocketIoServer],
|
|
executor: ['factory', Executor.factory],
|
|
// TODO: Deprecated. Remove in the next major
|
|
customFileHandlers: ['value', []],
|
|
reporter: ['factory', reporter.createReporters],
|
|
capturedBrowsers: ['factory', BrowserCollection.factory],
|
|
args: ['value', {}],
|
|
timer: ['value', {
|
|
setTimeout () {
|
|
return setTimeout.apply(root, arguments)
|
|
},
|
|
clearTimeout
|
|
}]
|
|
}]
|
|
|
|
this.on('load_error', (type, name) => {
|
|
this.log.debug(`Registered a load error of type ${type} with name ${name}`)
|
|
this.loadErrors.push([type, name])
|
|
})
|
|
|
|
modules = modules.concat(plugin.resolve(config.plugins, this))
|
|
this._injector = new di.Injector(modules)
|
|
}
|
|
|
|
async start () {
|
|
const config = this.get('config')
|
|
try {
|
|
this._boundServer = await NetUtils.bindAvailablePort(config.port, config.listenAddress)
|
|
this._boundServer.on('connection', (socket) => {
|
|
// Attach an error handler to avoid UncaughtException errors.
|
|
socket.on('error', (err) => {
|
|
// Errors on this socket are retried, ignore them
|
|
this.log.debug('Ignoring error on webserver connection: ' + err)
|
|
})
|
|
})
|
|
config.port = this._boundServer.address().port
|
|
await this._injector.invoke(this._start, this)
|
|
} catch (err) {
|
|
this.log.error(`Server start failed on port ${config.port}: ${err}`)
|
|
this._close(1)
|
|
}
|
|
}
|
|
|
|
get (token) {
|
|
return this._injector.get(token)
|
|
}
|
|
|
|
refreshFiles () {
|
|
return this._fileList ? this._fileList.refresh() : Promise.resolve()
|
|
}
|
|
|
|
refreshFile (path) {
|
|
return this._fileList ? this._fileList.changeFile(path) : Promise.resolve()
|
|
}
|
|
|
|
emitExitAsync (code) {
|
|
const name = 'exit'
|
|
let pending = this.listeners(name).length
|
|
const deferred = helper.defer()
|
|
|
|
function resolve () {
|
|
deferred.resolve(code)
|
|
}
|
|
|
|
try {
|
|
this.emit(name, (newCode) => {
|
|
if (newCode && typeof newCode === 'number') {
|
|
// Only update code if it is given and not zero
|
|
code = newCode
|
|
}
|
|
if (!--pending) {
|
|
resolve()
|
|
}
|
|
})
|
|
|
|
if (!pending) {
|
|
resolve()
|
|
}
|
|
} catch (err) {
|
|
deferred.reject(err)
|
|
}
|
|
return deferred.promise
|
|
}
|
|
|
|
async _start (config, launcher, preprocess, fileList, capturedBrowsers, executor, done) {
|
|
if (config.detached) {
|
|
this._detach(config, done)
|
|
return
|
|
}
|
|
|
|
this._fileList = fileList
|
|
|
|
await Promise.all(
|
|
config.frameworks.map((framework) => this._injector.get('framework:' + framework))
|
|
)
|
|
|
|
const webServer = this._injector.get('webServer')
|
|
const socketServer = this._injector.get('socketServer')
|
|
|
|
const singleRunDoneBrowsers = Object.create(null)
|
|
const singleRunBrowsers = new BrowserCollection(new EventEmitter())
|
|
let singleRunBrowserNotCaptured = false
|
|
|
|
webServer.on('error', (err) => {
|
|
this.log.error(`Webserver fail ${err}`)
|
|
this._close(1)
|
|
})
|
|
|
|
const afterPreprocess = () => {
|
|
if (config.autoWatch) {
|
|
const watcher = this.get('watcher')
|
|
this._injector.invoke(watcher)
|
|
}
|
|
|
|
webServer.listen(this._boundServer, () => {
|
|
this.log.info(`Karma v${constant.VERSION} server started at ${config.protocol}//${config.hostname}:${config.port}${config.urlRoot}`)
|
|
|
|
this.emit('listening', config.port)
|
|
if (config.browsers && config.browsers.length) {
|
|
this._injector.invoke(launcher.launch, launcher).forEach((browserLauncher) => {
|
|
singleRunDoneBrowsers[browserLauncher.id] = false
|
|
})
|
|
}
|
|
if (this.loadErrors.length > 0) {
|
|
this.log.error(new Error(`Found ${this.loadErrors.length} load error${this.loadErrors.length === 1 ? '' : 's'}`))
|
|
this._close(1)
|
|
}
|
|
})
|
|
}
|
|
|
|
fileList.refresh().then(afterPreprocess, (err) => {
|
|
this.log.error('Error during file loading or preprocessing\n' + err.stack || err)
|
|
afterPreprocess()
|
|
})
|
|
|
|
this.on('browsers_change', () => socketServer.sockets.emit('info', capturedBrowsers.serialize()))
|
|
|
|
this.on('browser_register', (browser) => {
|
|
launcher.markCaptured(browser.id)
|
|
|
|
if (launcher.areAllCaptured()) {
|
|
this.emit('browsers_ready')
|
|
|
|
if (config.autoWatch) {
|
|
executor.schedule()
|
|
}
|
|
}
|
|
})
|
|
|
|
if (config.browserConsoleLogOptions && config.browserConsoleLogOptions.path) {
|
|
const configLevel = config.browserConsoleLogOptions.level || 'debug'
|
|
const configFormat = config.browserConsoleLogOptions.format || '%b %T: %m'
|
|
const configPath = config.browserConsoleLogOptions.path
|
|
const configPathDir = path.dirname(configPath)
|
|
if (!fs.existsSync(configPathDir)) fs.mkdirSync(configPathDir, { recursive: true })
|
|
this.log.info(`Writing browser console to file: ${configPath}`)
|
|
const browserLogFile = fs.openSync(configPath, 'w+')
|
|
const levels = ['log', 'error', 'warn', 'info', 'debug']
|
|
this.on('browser_log', function (browser, message, level) {
|
|
if (levels.indexOf(level.toLowerCase()) > levels.indexOf(configLevel)) {
|
|
return
|
|
}
|
|
if (!helper.isString(message)) {
|
|
message = util.inspect(message, { showHidden: false, colors: false })
|
|
}
|
|
const logMap = { '%m': message, '%t': level.toLowerCase(), '%T': level.toUpperCase(), '%b': browser }
|
|
const logString = configFormat.replace(/%[mtTb]/g, (m) => logMap[m])
|
|
this.log.debug(`Writing browser console line: ${logString}`)
|
|
fs.writeSync(browserLogFile, logString + '\n')
|
|
})
|
|
}
|
|
|
|
socketServer.sockets.on('connection', (socket) => {
|
|
this.log.debug(`A browser has connected on socket ${socket.id}`)
|
|
|
|
const replySocketEvents = events.bufferEvents(socket, ['start', 'info', 'karma_error', 'result', 'complete'])
|
|
|
|
socket.on('error', (err) => {
|
|
this.log.debug('karma server socket error: ' + err)
|
|
})
|
|
|
|
socket.on('register', (info) => {
|
|
const knownBrowser = info.id ? (capturedBrowsers.getById(info.id) || singleRunBrowsers.getById(info.id)) : null
|
|
|
|
if (knownBrowser) {
|
|
knownBrowser.reconnect(socket, info.isSocketReconnect)
|
|
} else {
|
|
const newBrowser = this._injector.createChild([{
|
|
id: ['value', info.id || null],
|
|
fullName: ['value', (helper.isDefined(info.displayName) ? info.displayName : info.name)],
|
|
socket: ['value', socket]
|
|
}]).invoke(Browser.factory)
|
|
|
|
newBrowser.init()
|
|
|
|
if (config.singleRun) {
|
|
newBrowser.execute()
|
|
singleRunBrowsers.add(newBrowser)
|
|
}
|
|
}
|
|
|
|
replySocketEvents()
|
|
})
|
|
})
|
|
|
|
const emitRunCompleteIfAllBrowsersDone = () => {
|
|
if (Object.keys(singleRunDoneBrowsers).every((key) => singleRunDoneBrowsers[key])) {
|
|
this.emit('run_complete', singleRunBrowsers, singleRunBrowsers.getResults(singleRunBrowserNotCaptured, config))
|
|
}
|
|
}
|
|
|
|
this.on('browser_complete', (completedBrowser) => {
|
|
if (completedBrowser.lastResult.disconnected && completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
|
|
this.log.info(`Restarting ${completedBrowser.name} (${completedBrowser.disconnectsCount} of ${config.browserDisconnectTolerance} attempts)`)
|
|
|
|
if (!launcher.restart(completedBrowser.id)) {
|
|
this.emit('browser_restart_failure', completedBrowser)
|
|
}
|
|
} else {
|
|
this.emit('browser_complete_with_no_more_retries', completedBrowser)
|
|
}
|
|
})
|
|
|
|
this.on('stop', (done) => {
|
|
this.log.debug('Received stop event, exiting.')
|
|
this._close()
|
|
done()
|
|
})
|
|
|
|
if (config.singleRun) {
|
|
this.on('browser_restart_failure', (completedBrowser) => {
|
|
singleRunDoneBrowsers[completedBrowser.id] = true
|
|
emitRunCompleteIfAllBrowsersDone()
|
|
})
|
|
|
|
// This is the normal exit trigger.
|
|
this.on('browser_complete_with_no_more_retries', function (completedBrowser) {
|
|
singleRunDoneBrowsers[completedBrowser.id] = true
|
|
|
|
if (launcher.kill(completedBrowser.id)) {
|
|
completedBrowser.remove()
|
|
}
|
|
|
|
emitRunCompleteIfAllBrowsersDone()
|
|
})
|
|
|
|
this.on('browser_process_failure', (browserLauncher) => {
|
|
singleRunDoneBrowsers[browserLauncher.id] = true
|
|
singleRunBrowserNotCaptured = true
|
|
|
|
emitRunCompleteIfAllBrowsersDone()
|
|
})
|
|
|
|
this.on('run_complete', (browsers, results) => {
|
|
this.log.debug('Run complete, exiting.')
|
|
this._close(results.exitCode)
|
|
})
|
|
|
|
this.emit('run_start', singleRunBrowsers)
|
|
}
|
|
|
|
if (config.autoWatch) {
|
|
this.on('file_list_modified', () => {
|
|
this.log.debug('List of files has changed, trying to execute')
|
|
if (config.restartOnFileChange) {
|
|
socketServer.sockets.emit('stop')
|
|
}
|
|
executor.schedule()
|
|
})
|
|
}
|
|
|
|
processWrapper.on('SIGINT', () => this._close())
|
|
processWrapper.on('SIGTERM', () => this._close())
|
|
|
|
const reportError = (error) => {
|
|
this.log.error(error)
|
|
process.emit('infrastructure_error', error)
|
|
this._close(1)
|
|
}
|
|
|
|
processWrapper.on('unhandledRejection', (error) => {
|
|
this.log.error(`UnhandledRejection: ${error.stack || error.message || String(error)}`)
|
|
reportError(error)
|
|
})
|
|
|
|
processWrapper.on('uncaughtException', (error) => {
|
|
this.log.error(`UncaughtException: ${error.stack || error.message || String(error)}`)
|
|
reportError(error)
|
|
})
|
|
}
|
|
|
|
_detach (config, done) {
|
|
const tmpFile = tmp.fileSync({ keep: true })
|
|
this.log.info('Starting karma detached')
|
|
this.log.info('Run "karma stop" to stop the server.')
|
|
this.log.debug(`Writing config to tmp-file ${tmpFile.name}`)
|
|
config.detached = false
|
|
try {
|
|
fs.writeFileSync(tmpFile.name, JSON.stringify(config), 'utf8')
|
|
} catch (e) {
|
|
this.log.error("Couldn't write temporary configuration file")
|
|
done(1)
|
|
return
|
|
}
|
|
const child = spawn(process.argv[0], [path.resolve(__dirname, '../lib/detached.js'), tmpFile.name], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
})
|
|
child.unref()
|
|
}
|
|
|
|
/**
|
|
* Cleanup all resources allocated by Karma and call the `done` callback
|
|
* with the result of the tests execution.
|
|
*
|
|
* @param [exitCode] - Optional exit code. If omitted will be computed by
|
|
* 'exit' event listeners.
|
|
*/
|
|
_close (exitCode) {
|
|
const webServer = this._injector.get('webServer')
|
|
const socketServer = this._injector.get('socketServer')
|
|
const done = this._injector.get('done')
|
|
|
|
const webServerCloseTimeout = 3000
|
|
const sockets = socketServer.sockets.sockets
|
|
|
|
Object.keys(sockets).forEach((id) => {
|
|
const socket = sockets[id]
|
|
socket.removeAllListeners('disconnect')
|
|
if (!socket.disconnected) {
|
|
process.nextTick(socket.disconnect.bind(socket))
|
|
}
|
|
})
|
|
|
|
this.emitExitAsync(exitCode).catch((err) => {
|
|
this.log.error('Error while calling exit event listeners\n' + err.stack || err)
|
|
return 1
|
|
}).then((code) => {
|
|
socketServer.sockets.removeAllListeners()
|
|
socketServer.close()
|
|
|
|
let removeAllListenersDone = false
|
|
const removeAllListeners = () => {
|
|
if (removeAllListenersDone) {
|
|
return
|
|
}
|
|
removeAllListenersDone = true
|
|
webServer.removeAllListeners()
|
|
processWrapper.removeAllListeners()
|
|
done(code || 0)
|
|
}
|
|
|
|
const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
|
|
|
|
webServer.close(() => {
|
|
clearTimeout(closeTimeout)
|
|
removeAllListeners()
|
|
})
|
|
})
|
|
}
|
|
|
|
stop () {
|
|
return this.emitAsync('stop')
|
|
}
|
|
}
|
|
|
|
Server.prototype._start.$inject = ['config', 'launcher', 'preprocess', 'fileList', 'capturedBrowsers', 'executor', 'done']
|
|
|
|
module.exports = Server
|