-
Notifications
You must be signed in to change notification settings - Fork 190
Expand file tree
/
Copy pathbs-wrapper.ts
More file actions
147 lines (125 loc) · 4.62 KB
/
Copy pathbs-wrapper.ts
File metadata and controls
147 lines (125 loc) · 4.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// This wrapper script waits until a BrowserStack parallel session is available before launching the
// test command, to avoid overloading the service and making tests more flaky than necessary. This
// is also handled by the CI (resource groups), but it is helpful when launching tests outside of
// the CI.
//
// It used to re-run the test command based on its output (in particular, when the BrowserStack
// session failed to be created), but we observed that:
//
// * The retry logic of karma and wdio was more efficient to retry this kind of tests (the
// BrowserStack connection is re-created on each retry)
//
// * Aborting the test command via a SIGTERM signal was buggy and the command continued to run even
// after killing it. There might be a better way of prematurely aborting the test command if we need
// to in the future.
import { randomUUID } from 'node:crypto'
import { spawn, type ChildProcess } from 'node:child_process'
import browserStack from 'browserstack-local'
import { printLog, runMain, timeout, printError } from '../lib/executionUtils.ts'
import { command } from '../lib/command.ts'
import { browserStackRequest } from '../lib/bsUtils.ts'
const AVAILABILITY_CHECK_DELAY = 30_000
const NO_OUTPUT_TIMEOUT = 5 * 60_000
const BS_PLAN_URL = 'https://api.browserstack.com/automate/plan.json'
const bsLocal = new browserStack.Local()
const localIdentifier = `browser-sdk-${randomUUID()}`
runMain(async () => {
if (command`git tag --points-at HEAD`.run()) {
printLog('Skip bs execution on tags')
return
}
if (!process.env.BS_USERNAME || !process.env.BS_ACCESS_KEY) {
printError('Missing Browserstack credentials (BS_ACCESS_KEY and BS_USERNAME env variables)')
return
}
await waitForAvailability()
await startBsLocal()
const isSuccess = await runTests()
await stopBsLocal()
process.exit(isSuccess ? 0 : 1)
})
async function waitForAvailability(): Promise<void> {
while (await isAtCapacity()) {
const jitter = Math.floor(Math.random() * 3_000)
printLog('All BrowserStack sessions occupied, waiting...')
await timeout(AVAILABILITY_CHECK_DELAY + jitter)
}
}
async function isAtCapacity(): Promise<boolean> {
const plan = (await browserStackRequest(BS_PLAN_URL)) as {
parallel_sessions_running: number
parallel_sessions_max_allowed: number
}
return plan.parallel_sessions_running >= plan.parallel_sessions_max_allowed
}
function startBsLocal(): Promise<void> {
printLog('Starting BrowserStackLocal...')
return new Promise((resolve) => {
bsLocal.start(
{
key: process.env.BS_ACCESS_KEY,
localIdentifier,
forceLocal: true,
onlyAutomate: true,
},
(error?: Error) => {
if (error) {
printError('Failed to start BrowserStackLocal:', error)
process.exit(1)
}
printLog('BrowserStackLocal started', bsLocal.isRunning())
resolve()
}
)
})
}
function stopBsLocal(): Promise<void> {
return new Promise((resolve) => {
bsLocal.stop(() => {
printLog('BrowserStackLocal stopped')
resolve()
})
})
}
function runTests(): Promise<boolean> {
return new Promise((resolve) => {
const [command, ...args] = process.argv.slice(2)
const child: ChildProcess = spawn(command, args, {
stdio: ['inherit', 'pipe', 'pipe'],
env: {
...process.env,
FORCE_COLOR: 'true',
BROWSER_STACK: 'true',
BROWSERSTACK_LOCAL_IDENTIFIER: localIdentifier,
},
})
let output = ''
let timeoutId: NodeJS.Timeout
child.stdout!.pipe(process.stdout)
child.stdout!.on('data', onOutput)
child.stderr!.pipe(process.stderr)
child.stderr!.on('data', onOutput)
child.on('exit', (code, signal) => {
resolve(!signal && code === 0)
})
function onOutput(data: Buffer | string): void {
output += data.toString()
clearTimeout(timeoutId)
if (hasUnrecoverableFailure(output)) {
killIt('unrecoverable failure')
} else {
timeoutId = setTimeout(() => killIt('no output timeout'), NO_OUTPUT_TIMEOUT)
}
}
function killIt(message: string): void {
printError(`Killing the browserstack job because of ${message}`)
// use 'SIGKILL' instead of 'SIGTERM' because Karma intercepts 'SIGTERM' and terminates the process with a 0 exit code,
// which is not what we want here (we want to indicate a failure).
// see https://github.com/karma-runner/karma/blob/master/lib/server.js#L391
child.kill('SIGKILL')
}
})
}
function hasUnrecoverableFailure(stdout: string): boolean {
return stdout.includes('is set to true but local testing through BrowserStack is not connected.')
}