mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-09-28 13:13:37 +08:00
In github.com/karma-runner/karma/pull/3852 it is suggested that the 30s timeout issue with Karma can be seen when: 1. All the tests complete, which sets the state of Karma to "disconnected" 2. But there is some async activity which logs, or something. This causes Karma to restart its 30s timeout, but no more messages will be received. This is a bit experimental, but trying this patch to see if it has a positive impact on CQ. Bug: 406729910 Change-Id: Iaaa9247c801e193e5782a0da4857d804bcc45a92 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6407593 Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
274 lines
8.5 KiB
JavaScript
274 lines
8.5 KiB
JavaScript
'use strict'
|
|
|
|
const BrowserResult = require('./browser_result')
|
|
const helper = require('./helper')
|
|
const logger = require('./logger')
|
|
|
|
const CONNECTED = 'CONNECTED' // The browser is connected but not yet been commanded to execute tests.
|
|
const CONFIGURING = 'CONFIGURING' // The browser has been told to execute tests; it is configuring before tests execution.
|
|
const EXECUTING = 'EXECUTING' // The browser is executing the tests.
|
|
const EXECUTING_DISCONNECTED = 'EXECUTING_DISCONNECTED' // The browser is executing the tests, but temporarily disconnect (waiting for socket reconnecting).
|
|
const DISCONNECTED = 'DISCONNECTED' // The browser got completely disconnected (e.g. browser crash) and can be only restored with a restart of execution.
|
|
|
|
class Browser {
|
|
constructor (id, fullName, collection, emitter, socket, timer, disconnectDelay,
|
|
noActivityTimeout, singleRun, clientConfig) {
|
|
this.id = id
|
|
this.fullName = fullName
|
|
this.name = helper.browserFullNameToShort(fullName)
|
|
this.lastResult = new BrowserResult()
|
|
this.disconnectsCount = 0
|
|
this.activeSockets = [socket]
|
|
this.noActivityTimeout = noActivityTimeout
|
|
this.singleRun = singleRun
|
|
this.clientConfig = clientConfig
|
|
this.collection = collection
|
|
this.emitter = emitter
|
|
this.socket = socket
|
|
this.timer = timer
|
|
this.disconnectDelay = disconnectDelay
|
|
|
|
this.log = logger.create(this.name)
|
|
|
|
this.noActivityTimeoutId = null
|
|
this.pendingDisconnect = null
|
|
this.setState(CONNECTED)
|
|
}
|
|
|
|
init () {
|
|
this.log.info(`Connected on socket ${this.socket.id} with id ${this.id}`)
|
|
|
|
this.bindSocketEvents(this.socket)
|
|
this.collection.add(this)
|
|
this.emitter.emit('browser_register', this)
|
|
}
|
|
|
|
setState (toState) {
|
|
this.log.debug(`${this.state} -> ${toState}`)
|
|
this.state = toState
|
|
}
|
|
|
|
onKarmaError (error) {
|
|
if (this.isNotConnected()) {
|
|
this.lastResult.error = true
|
|
}
|
|
this.emitter.emit('browser_error', this, error)
|
|
this.refreshNoActivityTimeout()
|
|
}
|
|
|
|
onInfo (info) {
|
|
if (helper.isDefined(info.dump)) {
|
|
this.emitter.emit('browser_log', this, info.dump, 'dump')
|
|
}
|
|
|
|
if (helper.isDefined(info.log)) {
|
|
this.emitter.emit('browser_log', this, info.log, info.type)
|
|
} else if (helper.isDefined(info.total)) {
|
|
if (this.state === EXECUTING) {
|
|
this.lastResult.total = info.total
|
|
}
|
|
} else if (!helper.isDefined(info.dump)) {
|
|
this.emitter.emit('browser_info', this, info)
|
|
}
|
|
|
|
this.refreshNoActivityTimeout()
|
|
}
|
|
|
|
onStart (info) {
|
|
if (info.total === null) {
|
|
this.log.warn('Adapter did not report total number of specs.')
|
|
}
|
|
|
|
this.lastResult = new BrowserResult(info.total)
|
|
this.setState(EXECUTING)
|
|
this.emitter.emit('browser_start', this, info)
|
|
this.refreshNoActivityTimeout()
|
|
}
|
|
|
|
onComplete (result) {
|
|
if (this.isNotConnected()) {
|
|
this.setState(CONNECTED)
|
|
this.lastResult.totalTimeEnd()
|
|
|
|
this.emitter.emit('browsers_change', this.collection)
|
|
this.emitter.emit('browser_complete', this, result)
|
|
|
|
this.clearNoActivityTimeout()
|
|
}
|
|
}
|
|
|
|
onSocketDisconnect (reason, disconnectedSocket) {
|
|
helper.arrayRemove(this.activeSockets, disconnectedSocket)
|
|
if (this.activeSockets.length) {
|
|
this.log.debug(`Disconnected ${disconnectedSocket.id}, still have ${this.getActiveSocketsIds()}`)
|
|
return
|
|
}
|
|
|
|
if (this.isConnected()) {
|
|
this.disconnect(`Client disconnected from CONNECTED state (${reason})`)
|
|
} else if ([CONFIGURING, EXECUTING].includes(this.state)) {
|
|
this.log.debug(`Disconnected during run, waiting ${this.disconnectDelay}ms for reconnecting.`)
|
|
this.setState(EXECUTING_DISCONNECTED)
|
|
|
|
this.pendingDisconnect = this.timer.setTimeout(() => {
|
|
this.lastResult.totalTimeEnd()
|
|
this.lastResult.disconnected = true
|
|
this.disconnect(`reconnect failed before timeout of ${this.disconnectDelay}ms (${reason})`)
|
|
this.emitter.emit('browser_complete', this)
|
|
}, this.disconnectDelay)
|
|
|
|
this.clearNoActivityTimeout()
|
|
}
|
|
}
|
|
|
|
reconnect (newSocket, clientSaysReconnect) {
|
|
if (!clientSaysReconnect || this.state === DISCONNECTED) {
|
|
this.log.info(`Disconnected browser returned on socket ${newSocket.id} with id ${this.id}.`)
|
|
this.setState(CONNECTED)
|
|
|
|
// The disconnected browser is already part of the collection.
|
|
// Update the collection view in the UI (header on client.html)
|
|
this.emitter.emit('browsers_change', this.collection)
|
|
// Notify the launcher
|
|
this.emitter.emit('browser_register', this)
|
|
// Execute tests if configured to do so.
|
|
if (this.singleRun) {
|
|
this.execute()
|
|
}
|
|
} else if (this.state === EXECUTING_DISCONNECTED) {
|
|
this.log.debug('Lost socket connection, but browser continued to execute. Reconnected ' +
|
|
`on socket ${newSocket.id}.`)
|
|
this.setState(EXECUTING)
|
|
} else if ([CONNECTED, CONFIGURING, EXECUTING].includes(this.state)) {
|
|
this.log.debug(`Rebinding to new socket ${newSocket.id} (already have ` +
|
|
`${this.getActiveSocketsIds()})`)
|
|
}
|
|
|
|
if (!this.activeSockets.some((s) => s.id === newSocket.id)) {
|
|
this.activeSockets.push(newSocket)
|
|
this.bindSocketEvents(newSocket)
|
|
}
|
|
|
|
if (this.pendingDisconnect) {
|
|
this.timer.clearTimeout(this.pendingDisconnect)
|
|
}
|
|
|
|
this.refreshNoActivityTimeout()
|
|
}
|
|
|
|
onResult (result) {
|
|
if (Array.isArray(result)) {
|
|
result.forEach(this.onResult, this)
|
|
} else if (this.isNotConnected()) {
|
|
this.lastResult.add(result)
|
|
this.emitter.emit('spec_complete', this, result)
|
|
}
|
|
this.refreshNoActivityTimeout()
|
|
}
|
|
|
|
execute () {
|
|
this.activeSockets.forEach((socket) => socket.emit('execute', this.clientConfig))
|
|
this.setState(CONFIGURING)
|
|
this.refreshNoActivityTimeout()
|
|
}
|
|
|
|
getActiveSocketsIds () {
|
|
return this.activeSockets.map((s) => s.id).join(', ')
|
|
}
|
|
|
|
disconnect (reason) {
|
|
this.log.warn(`Disconnected (${this.disconnectsCount} times) ${reason || ''}`)
|
|
this.disconnectsCount++
|
|
this.emitter.emit('browser_error', this, `Disconnected ${reason || ''}`)
|
|
this.remove()
|
|
}
|
|
|
|
remove () {
|
|
this.setState(DISCONNECTED)
|
|
this.collection.remove(this)
|
|
}
|
|
|
|
refreshNoActivityTimeout () {
|
|
if (this.noActivityTimeout) {
|
|
this.clearNoActivityTimeout()
|
|
|
|
if(this.state !== 'DISCONNECTED') {
|
|
this.noActivityTimeoutId = this.timer.setTimeout(() => {
|
|
this.lastResult.totalTimeEnd()
|
|
this.lastResult.disconnected = true
|
|
this.disconnect(`, because no message in ${this.noActivityTimeout} ms.`)
|
|
this.emitter.emit('browser_complete', this)
|
|
}, this.noActivityTimeout)
|
|
}
|
|
}
|
|
}
|
|
|
|
clearNoActivityTimeout () {
|
|
if (this.noActivityTimeout && this.noActivityTimeoutId) {
|
|
this.timer.clearTimeout(this.noActivityTimeoutId)
|
|
this.noActivityTimeoutId = null
|
|
}
|
|
}
|
|
|
|
bindSocketEvents (socket) {
|
|
// TODO: check which of these events are actually emitted by socket
|
|
socket.on('disconnect', (reason) => this.onSocketDisconnect(reason, socket))
|
|
socket.on('start', (info) => this.onStart(info))
|
|
socket.on('karma_error', (error) => this.onKarmaError(error))
|
|
socket.on('complete', (result) => this.onComplete(result))
|
|
socket.on('info', (info) => this.onInfo(info))
|
|
socket.on('result', (result) => this.onResult(result))
|
|
}
|
|
|
|
isConnected () {
|
|
return this.state === CONNECTED
|
|
}
|
|
|
|
isNotConnected () {
|
|
return !this.isConnected()
|
|
}
|
|
|
|
serialize () {
|
|
return {
|
|
id: this.id,
|
|
name: this.name,
|
|
isConnected: this.state === CONNECTED
|
|
}
|
|
}
|
|
|
|
toString () {
|
|
return this.name
|
|
}
|
|
|
|
toJSON () {
|
|
return {
|
|
id: this.id,
|
|
fullName: this.fullName,
|
|
name: this.name,
|
|
state: this.state,
|
|
lastResult: this.lastResult,
|
|
disconnectsCount: this.disconnectsCount,
|
|
noActivityTimeout: this.noActivityTimeout,
|
|
disconnectDelay: this.disconnectDelay
|
|
}
|
|
}
|
|
}
|
|
|
|
Browser.factory = function (
|
|
id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
|
|
/* config.browserDisconnectTimeout */ disconnectDelay,
|
|
/* config.browserNoActivityTimeout */ noActivityTimeout,
|
|
/* config.singleRun */ singleRun,
|
|
/* config.client */ clientConfig) {
|
|
return new Browser(id, fullName, collection, emitter, socket, timer,
|
|
disconnectDelay, noActivityTimeout, singleRun, clientConfig)
|
|
}
|
|
|
|
Browser.STATE_CONNECTED = CONNECTED
|
|
Browser.STATE_CONFIGURING = CONFIGURING
|
|
Browser.STATE_EXECUTING = EXECUTING
|
|
Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED
|
|
Browser.STATE_DISCONNECTED = DISCONNECTED
|
|
|
|
module.exports = Browser
|