[6a3a178] | 1 | 'use strict'
|
---|
| 2 |
|
---|
| 3 | const BrowserResult = require('./browser_result')
|
---|
| 4 | const helper = require('./helper')
|
---|
| 5 | const logger = require('./logger')
|
---|
| 6 |
|
---|
| 7 | const CONNECTED = 'CONNECTED' // The browser is connected but not yet been commanded to execute tests.
|
---|
| 8 | const CONFIGURING = 'CONFIGURING' // The browser has been told to execute tests; it is configuring before tests execution.
|
---|
| 9 | const EXECUTING = 'EXECUTING' // The browser is executing the tests.
|
---|
| 10 | const EXECUTING_DISCONNECTED = 'EXECUTING_DISCONNECTED' // The browser is executing the tests, but temporarily disconnect (waiting for socket reconnecting).
|
---|
| 11 | const DISCONNECTED = 'DISCONNECTED' // The browser got completely disconnected (e.g. browser crash) and can be only restored with a restart of execution.
|
---|
| 12 |
|
---|
| 13 | class Browser {
|
---|
| 14 | constructor (id, fullName, collection, emitter, socket, timer, disconnectDelay,
|
---|
| 15 | noActivityTimeout, singleRun, clientConfig) {
|
---|
| 16 | this.id = id
|
---|
| 17 | this.fullName = fullName
|
---|
| 18 | this.name = helper.browserFullNameToShort(fullName)
|
---|
| 19 | this.lastResult = new BrowserResult()
|
---|
| 20 | this.disconnectsCount = 0
|
---|
| 21 | this.activeSockets = [socket]
|
---|
| 22 | this.noActivityTimeout = noActivityTimeout
|
---|
| 23 | this.singleRun = singleRun
|
---|
| 24 | this.clientConfig = clientConfig
|
---|
| 25 | this.collection = collection
|
---|
| 26 | this.emitter = emitter
|
---|
| 27 | this.socket = socket
|
---|
| 28 | this.timer = timer
|
---|
| 29 | this.disconnectDelay = disconnectDelay
|
---|
| 30 |
|
---|
| 31 | this.log = logger.create(this.name)
|
---|
| 32 |
|
---|
| 33 | this.noActivityTimeoutId = null
|
---|
| 34 | this.pendingDisconnect = null
|
---|
| 35 | this.setState(CONNECTED)
|
---|
| 36 | }
|
---|
| 37 |
|
---|
| 38 | init () {
|
---|
| 39 | this.log.info(`Connected on socket ${this.socket.id} with id ${this.id}`)
|
---|
| 40 |
|
---|
| 41 | this.bindSocketEvents(this.socket)
|
---|
| 42 | this.collection.add(this)
|
---|
| 43 | this.emitter.emit('browser_register', this)
|
---|
| 44 | }
|
---|
| 45 |
|
---|
| 46 | setState (toState) {
|
---|
| 47 | this.log.debug(`${this.state} -> ${toState}`)
|
---|
| 48 | this.state = toState
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | onKarmaError (error) {
|
---|
| 52 | if (this.isNotConnected()) {
|
---|
| 53 | this.lastResult.error = true
|
---|
| 54 | }
|
---|
| 55 | this.emitter.emit('browser_error', this, error)
|
---|
| 56 | this.refreshNoActivityTimeout()
|
---|
| 57 | }
|
---|
| 58 |
|
---|
| 59 | onInfo (info) {
|
---|
| 60 | if (helper.isDefined(info.dump)) {
|
---|
| 61 | this.emitter.emit('browser_log', this, info.dump, 'dump')
|
---|
| 62 | }
|
---|
| 63 |
|
---|
| 64 | if (helper.isDefined(info.log)) {
|
---|
| 65 | this.emitter.emit('browser_log', this, info.log, info.type)
|
---|
| 66 | } else if (helper.isDefined(info.total)) {
|
---|
| 67 | if (this.state === EXECUTING) {
|
---|
| 68 | this.lastResult.total = info.total
|
---|
| 69 | }
|
---|
| 70 | } else if (!helper.isDefined(info.dump)) {
|
---|
| 71 | this.emitter.emit('browser_info', this, info)
|
---|
| 72 | }
|
---|
| 73 |
|
---|
| 74 | this.refreshNoActivityTimeout()
|
---|
| 75 | }
|
---|
| 76 |
|
---|
| 77 | onStart (info) {
|
---|
| 78 | if (info.total === null) {
|
---|
| 79 | this.log.warn('Adapter did not report total number of specs.')
|
---|
| 80 | }
|
---|
| 81 |
|
---|
| 82 | this.lastResult = new BrowserResult(info.total)
|
---|
| 83 | this.setState(EXECUTING)
|
---|
| 84 | this.emitter.emit('browser_start', this, info)
|
---|
| 85 | this.refreshNoActivityTimeout()
|
---|
| 86 | }
|
---|
| 87 |
|
---|
| 88 | onComplete (result) {
|
---|
| 89 | if (this.isNotConnected()) {
|
---|
| 90 | this.setState(CONNECTED)
|
---|
| 91 | this.lastResult.totalTimeEnd()
|
---|
| 92 |
|
---|
| 93 | this.emitter.emit('browsers_change', this.collection)
|
---|
| 94 | this.emitter.emit('browser_complete', this, result)
|
---|
| 95 |
|
---|
| 96 | this.clearNoActivityTimeout()
|
---|
| 97 | }
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | onSocketDisconnect (reason, disconnectedSocket) {
|
---|
| 101 | helper.arrayRemove(this.activeSockets, disconnectedSocket)
|
---|
| 102 | if (this.activeSockets.length) {
|
---|
| 103 | this.log.debug(`Disconnected ${disconnectedSocket.id}, still have ${this.getActiveSocketsIds()}`)
|
---|
| 104 | return
|
---|
| 105 | }
|
---|
| 106 |
|
---|
| 107 | if (this.isConnected()) {
|
---|
| 108 | this.disconnect(`Client disconnected from CONNECTED state (${reason})`)
|
---|
| 109 | } else if ([CONFIGURING, EXECUTING].includes(this.state)) {
|
---|
| 110 | this.log.debug(`Disconnected during run, waiting ${this.disconnectDelay}ms for reconnecting.`)
|
---|
| 111 | this.setState(EXECUTING_DISCONNECTED)
|
---|
| 112 |
|
---|
| 113 | this.pendingDisconnect = this.timer.setTimeout(() => {
|
---|
| 114 | this.lastResult.totalTimeEnd()
|
---|
| 115 | this.lastResult.disconnected = true
|
---|
| 116 | this.disconnect(`reconnect failed before timeout of ${this.disconnectDelay}ms (${reason})`)
|
---|
| 117 | this.emitter.emit('browser_complete', this)
|
---|
| 118 | }, this.disconnectDelay)
|
---|
| 119 |
|
---|
| 120 | this.clearNoActivityTimeout()
|
---|
| 121 | }
|
---|
| 122 | }
|
---|
| 123 |
|
---|
| 124 | reconnect (newSocket, clientSaysReconnect) {
|
---|
| 125 | if (!clientSaysReconnect || this.state === DISCONNECTED) {
|
---|
| 126 | this.log.info(`Disconnected browser returned on socket ${newSocket.id} with id ${this.id}.`)
|
---|
| 127 | this.setState(CONNECTED)
|
---|
| 128 |
|
---|
| 129 | // The disconnected browser is already part of the collection.
|
---|
| 130 | // Update the collection view in the UI (header on client.html)
|
---|
| 131 | this.emitter.emit('browsers_change', this.collection)
|
---|
| 132 | // Notify the launcher
|
---|
| 133 | this.emitter.emit('browser_register', this)
|
---|
| 134 | // Execute tests if configured to do so.
|
---|
| 135 | if (this.singleRun) {
|
---|
| 136 | this.execute()
|
---|
| 137 | }
|
---|
| 138 | } else if (this.state === EXECUTING_DISCONNECTED) {
|
---|
| 139 | this.log.debug('Lost socket connection, but browser continued to execute. Reconnected ' +
|
---|
| 140 | `on socket ${newSocket.id}.`)
|
---|
| 141 | this.setState(EXECUTING)
|
---|
| 142 | } else if ([CONNECTED, CONFIGURING, EXECUTING].includes(this.state)) {
|
---|
| 143 | this.log.debug(`Rebinding to new socket ${newSocket.id} (already have ` +
|
---|
| 144 | `${this.getActiveSocketsIds()})`)
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | if (!this.activeSockets.some((s) => s.id === newSocket.id)) {
|
---|
| 148 | this.activeSockets.push(newSocket)
|
---|
| 149 | this.bindSocketEvents(newSocket)
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | if (this.pendingDisconnect) {
|
---|
| 153 | this.timer.clearTimeout(this.pendingDisconnect)
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 | this.refreshNoActivityTimeout()
|
---|
| 157 | }
|
---|
| 158 |
|
---|
| 159 | onResult (result) {
|
---|
| 160 | if (Array.isArray(result)) {
|
---|
| 161 | result.forEach(this.onResult, this)
|
---|
| 162 | } else if (this.isNotConnected()) {
|
---|
| 163 | this.lastResult.add(result)
|
---|
| 164 | this.emitter.emit('spec_complete', this, result)
|
---|
| 165 | }
|
---|
| 166 | this.refreshNoActivityTimeout()
|
---|
| 167 | }
|
---|
| 168 |
|
---|
| 169 | execute () {
|
---|
| 170 | this.activeSockets.forEach((socket) => socket.emit('execute', this.clientConfig))
|
---|
| 171 | this.setState(CONFIGURING)
|
---|
| 172 | this.refreshNoActivityTimeout()
|
---|
| 173 | }
|
---|
| 174 |
|
---|
| 175 | getActiveSocketsIds () {
|
---|
| 176 | return this.activeSockets.map((s) => s.id).join(', ')
|
---|
| 177 | }
|
---|
| 178 |
|
---|
| 179 | disconnect (reason) {
|
---|
| 180 | this.log.warn(`Disconnected (${this.disconnectsCount} times) ${reason || ''}`)
|
---|
| 181 | this.disconnectsCount++
|
---|
| 182 | this.emitter.emit('browser_error', this, `Disconnected ${reason || ''}`)
|
---|
| 183 | this.remove()
|
---|
| 184 | }
|
---|
| 185 |
|
---|
| 186 | remove () {
|
---|
| 187 | this.setState(DISCONNECTED)
|
---|
| 188 | this.collection.remove(this)
|
---|
| 189 | }
|
---|
| 190 |
|
---|
| 191 | refreshNoActivityTimeout () {
|
---|
| 192 | if (this.noActivityTimeout) {
|
---|
| 193 | this.clearNoActivityTimeout()
|
---|
| 194 |
|
---|
| 195 | this.noActivityTimeoutId = this.timer.setTimeout(() => {
|
---|
| 196 | this.lastResult.totalTimeEnd()
|
---|
| 197 | this.lastResult.disconnected = true
|
---|
| 198 | this.disconnect(`, because no message in ${this.noActivityTimeout} ms.`)
|
---|
| 199 | this.emitter.emit('browser_complete', this)
|
---|
| 200 | }, this.noActivityTimeout)
|
---|
| 201 | }
|
---|
| 202 | }
|
---|
| 203 |
|
---|
| 204 | clearNoActivityTimeout () {
|
---|
| 205 | if (this.noActivityTimeout && this.noActivityTimeoutId) {
|
---|
| 206 | this.timer.clearTimeout(this.noActivityTimeoutId)
|
---|
| 207 | this.noActivityTimeoutId = null
|
---|
| 208 | }
|
---|
| 209 | }
|
---|
| 210 |
|
---|
| 211 | bindSocketEvents (socket) {
|
---|
| 212 | // TODO: check which of these events are actually emitted by socket
|
---|
| 213 | socket.on('disconnect', (reason) => this.onSocketDisconnect(reason, socket))
|
---|
| 214 | socket.on('start', (info) => this.onStart(info))
|
---|
| 215 | socket.on('karma_error', (error) => this.onKarmaError(error))
|
---|
| 216 | socket.on('complete', (result) => this.onComplete(result))
|
---|
| 217 | socket.on('info', (info) => this.onInfo(info))
|
---|
| 218 | socket.on('result', (result) => this.onResult(result))
|
---|
| 219 | }
|
---|
| 220 |
|
---|
| 221 | isConnected () {
|
---|
| 222 | return this.state === CONNECTED
|
---|
| 223 | }
|
---|
| 224 |
|
---|
| 225 | isNotConnected () {
|
---|
| 226 | return !this.isConnected()
|
---|
| 227 | }
|
---|
| 228 |
|
---|
| 229 | serialize () {
|
---|
| 230 | return {
|
---|
| 231 | id: this.id,
|
---|
| 232 | name: this.name,
|
---|
| 233 | isConnected: this.state === CONNECTED
|
---|
| 234 | }
|
---|
| 235 | }
|
---|
| 236 |
|
---|
| 237 | toString () {
|
---|
| 238 | return this.name
|
---|
| 239 | }
|
---|
| 240 |
|
---|
| 241 | toJSON () {
|
---|
| 242 | return {
|
---|
| 243 | id: this.id,
|
---|
| 244 | fullName: this.fullName,
|
---|
| 245 | name: this.name,
|
---|
| 246 | state: this.state,
|
---|
| 247 | lastResult: this.lastResult,
|
---|
| 248 | disconnectsCount: this.disconnectsCount,
|
---|
| 249 | noActivityTimeout: this.noActivityTimeout,
|
---|
| 250 | disconnectDelay: this.disconnectDelay
|
---|
| 251 | }
|
---|
| 252 | }
|
---|
| 253 | }
|
---|
| 254 |
|
---|
| 255 | Browser.factory = function (
|
---|
| 256 | id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
|
---|
| 257 | /* config.browserDisconnectTimeout */ disconnectDelay,
|
---|
| 258 | /* config.browserNoActivityTimeout */ noActivityTimeout,
|
---|
| 259 | /* config.singleRun */ singleRun,
|
---|
| 260 | /* config.client */ clientConfig) {
|
---|
| 261 | return new Browser(id, fullName, collection, emitter, socket, timer,
|
---|
| 262 | disconnectDelay, noActivityTimeout, singleRun, clientConfig)
|
---|
| 263 | }
|
---|
| 264 |
|
---|
| 265 | Browser.STATE_CONNECTED = CONNECTED
|
---|
| 266 | Browser.STATE_CONFIGURING = CONFIGURING
|
---|
| 267 | Browser.STATE_EXECUTING = EXECUTING
|
---|
| 268 | Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED
|
---|
| 269 | Browser.STATE_DISCONNECTED = DISCONNECTED
|
---|
| 270 |
|
---|
| 271 | module.exports = Browser
|
---|