browser.js 8.5 KB

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