@amazon/vinyl-build-utils 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browserstack/browserStackConfig.d.ts +111 -0
- package/dist/browserstack/browserStackConfig.d.ts.map +1 -0
- package/dist/browserstack/browserStackConfig.js +26 -0
- package/dist/browserstack/browserStackRestApi.d.ts +2 -33
- package/dist/browserstack/browserStackRestApi.d.ts.map +1 -1
- package/dist/browserstack/browserStackRestApi.js +0 -21
- package/dist/esbuild/analyzeExports.d.ts.map +1 -1
- package/dist/esbuild/analyzeExports.js +37 -24
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vinyl/vinylDefaultBrowserStackOptions.d.ts +1 -1
- package/dist/vinyl/vinylDefaultBrowserStackOptions.js +2 -2
- package/dist/vinyl/vinylSupportedBrowsers.d.ts +1 -1
- package/dist/vinyl/vinylSupportedBrowsers.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/browserstack/BrowserStackWorker.d.ts +0 -158
- package/dist/browserstack/BrowserStackWorker.d.ts.map +0 -1
- package/dist/browserstack/BrowserStackWorker.js +0 -330
- package/dist/browserstack/BrowserStackWorkerController.d.ts +0 -103
- package/dist/browserstack/BrowserStackWorkerController.d.ts.map +0 -1
- package/dist/browserstack/BrowserStackWorkerController.js +0 -199
- package/dist/browserstack/runBrowserStack.d.ts +0 -123
- package/dist/browserstack/runBrowserStack.d.ts.map +0 -1
- package/dist/browserstack/runBrowserStack.js +0 -211
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
import { TaskQueue } from '../util/TaskQueue';
|
|
6
|
-
import { logger } from '../util/Logger';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import fs from 'node:fs';
|
|
9
|
-
import crypto from 'node:crypto';
|
|
10
|
-
export class BrowserStackWorker {
|
|
11
|
-
deps;
|
|
12
|
-
/**
|
|
13
|
-
* Set to observe state updates.
|
|
14
|
-
*/
|
|
15
|
-
onUpdate = () => { };
|
|
16
|
-
_state;
|
|
17
|
-
tasks = new TaskQueue();
|
|
18
|
-
pollIntervalId = null;
|
|
19
|
-
captureTimeoutId = null;
|
|
20
|
-
options;
|
|
21
|
-
/**
|
|
22
|
-
* The worker url with an appended reportApi query param.
|
|
23
|
-
*/
|
|
24
|
-
url;
|
|
25
|
-
constructor(deps, workerOptions, options) {
|
|
26
|
-
this.deps = deps;
|
|
27
|
-
this.tasks.onError = (error) => this.setError(error);
|
|
28
|
-
this.options = {
|
|
29
|
-
retries: 2,
|
|
30
|
-
captureTimeout: 45,
|
|
31
|
-
pollInterval: 5,
|
|
32
|
-
logsDir: './bStackLogs',
|
|
33
|
-
queryParams: null,
|
|
34
|
-
...options,
|
|
35
|
-
};
|
|
36
|
-
this._state = {
|
|
37
|
-
localId: crypto.randomUUID(),
|
|
38
|
-
workerOptions: workerOptions,
|
|
39
|
-
error: null,
|
|
40
|
-
session: null,
|
|
41
|
-
started: false,
|
|
42
|
-
active: false,
|
|
43
|
-
workerId: null,
|
|
44
|
-
sessionId: null,
|
|
45
|
-
totalSpecs: null,
|
|
46
|
-
totalCompleted: 0,
|
|
47
|
-
attemptsRemaining: this.options.retries,
|
|
48
|
-
};
|
|
49
|
-
// Add the reportApi query param:
|
|
50
|
-
const finalUrl = new URL(workerOptions.url);
|
|
51
|
-
finalUrl.searchParams.set('reportApi', `/report/${this._state.localId}`);
|
|
52
|
-
// Add the provided query params from options.
|
|
53
|
-
if (options?.queryParams) {
|
|
54
|
-
for (const [name, value] of Object.entries(options.queryParams)) {
|
|
55
|
-
finalUrl.searchParams.set(name, value);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
this.url = finalUrl.toString();
|
|
59
|
-
if (this.options.logsDir)
|
|
60
|
-
fs.mkdirSync(this.options.logsDir, { recursive: true });
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Returns true if this worker is completed. This will be true if either in an error state or the session status
|
|
64
|
-
* indicates completion
|
|
65
|
-
*/
|
|
66
|
-
get completed() {
|
|
67
|
-
const state = this._state;
|
|
68
|
-
const status = state.session?.status;
|
|
69
|
-
return (state.error != null ||
|
|
70
|
-
(state.started &&
|
|
71
|
-
status != null &&
|
|
72
|
-
['timeout', 'passed', 'failed'].includes(status)));
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Returns this worker's state.
|
|
76
|
-
*/
|
|
77
|
-
get state() {
|
|
78
|
-
return this._state;
|
|
79
|
-
}
|
|
80
|
-
get name() {
|
|
81
|
-
return this._state.workerOptions.name;
|
|
82
|
-
}
|
|
83
|
-
get passed() {
|
|
84
|
-
return this._state.session?.status === 'passed';
|
|
85
|
-
}
|
|
86
|
-
set totalCompleted(value) {
|
|
87
|
-
this._state.totalCompleted = value;
|
|
88
|
-
}
|
|
89
|
-
set totalSpecs(value) {
|
|
90
|
-
this._state.totalSpecs = value;
|
|
91
|
-
}
|
|
92
|
-
done(passed, reason) {
|
|
93
|
-
const name = this.name;
|
|
94
|
-
if (passed)
|
|
95
|
-
logger.info(`✅ ${name} passed`);
|
|
96
|
-
else
|
|
97
|
-
logger.info(`❌ ${name} failed\n${reason}\n`);
|
|
98
|
-
const client = this.deps.client;
|
|
99
|
-
return this.tasks
|
|
100
|
-
.enqueue(async () => {
|
|
101
|
-
const state = this._state;
|
|
102
|
-
const { sessionId, workerId } = state;
|
|
103
|
-
if (!sessionId || !workerId) {
|
|
104
|
-
logger.debug('done aborting - sessionId or workerId is missing');
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
logger.debug('done, passed:', passed);
|
|
108
|
-
state.session = await client.updateSession(sessionId, {
|
|
109
|
-
status: passed ? 'passed' : 'failed',
|
|
110
|
-
reason: reason,
|
|
111
|
-
});
|
|
112
|
-
this.onUpdate();
|
|
113
|
-
})
|
|
114
|
-
.finally(() => {
|
|
115
|
-
this.terminate().catch(() => { });
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
async refreshSession() {
|
|
119
|
-
const state = this._state;
|
|
120
|
-
if (state.sessionId) {
|
|
121
|
-
const previousStatus = state.session?.status;
|
|
122
|
-
state.session = await this.deps.client.getSession(state.sessionId);
|
|
123
|
-
// A worker terminated by BrowserStack (e.g. the run exceeded the
|
|
124
|
-
// worker `timeout`) reports a 'timeout' status. It counts as a
|
|
125
|
-
// failure like any other, but is otherwise silent — the reporter
|
|
126
|
-
// never sends its `done`, so nothing explains the failed count.
|
|
127
|
-
// Log it clearly, once, on the transition.
|
|
128
|
-
if (state.session.status === 'timeout' &&
|
|
129
|
-
previousStatus !== 'timeout') {
|
|
130
|
-
const secs = state.workerOptions.timeout;
|
|
131
|
-
logger.error(`⏱️ ${this.name} timed out${secs ? ` after ${secs}s` : ''} — worker terminated by BrowserStack before tests ` +
|
|
132
|
-
`completed; counts as a failure`);
|
|
133
|
-
}
|
|
134
|
-
this.onUpdate();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
appendLogs(logs) {
|
|
138
|
-
if (this.options.logsDir == null || logs.length === 0)
|
|
139
|
-
return;
|
|
140
|
-
const logFilePath = path.join(this.options.logsDir, `${this._state.sessionId}.log`);
|
|
141
|
-
// Append logs to a file.
|
|
142
|
-
fs.appendFile(logFilePath, logs.map((entry) => entry.join(' ')).join('\n') + '\n', (error) => {
|
|
143
|
-
if (error)
|
|
144
|
-
logger.error('Could not write to console log file:', error);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Creates the worker/session.
|
|
149
|
-
*/
|
|
150
|
-
async start() {
|
|
151
|
-
logger.debug(`Starting session '${this.name}'...`);
|
|
152
|
-
const state = this._state;
|
|
153
|
-
state.started = true;
|
|
154
|
-
await this.tasks
|
|
155
|
-
.enqueue(async () => {
|
|
156
|
-
if (state.active)
|
|
157
|
-
return;
|
|
158
|
-
state.active = true;
|
|
159
|
-
this.onUpdate();
|
|
160
|
-
this.pollIntervalId = setInterval(() => {
|
|
161
|
-
void this.refreshSession();
|
|
162
|
-
}, this.options.pollInterval * 1000);
|
|
163
|
-
this.captureTimeoutId = setTimeout(this.captureTimeoutHandler, this.options.captureTimeout * 1000);
|
|
164
|
-
const client = this.deps.client;
|
|
165
|
-
// fallbackBrowser is a local concern, not a BrowserStack capability.
|
|
166
|
-
const { fallbackBrowser: _fallbackBrowser, ...workerOptions } = state.workerOptions;
|
|
167
|
-
const newWorker = await client.createWorker({
|
|
168
|
-
...workerOptions,
|
|
169
|
-
url: this.url,
|
|
170
|
-
});
|
|
171
|
-
state.workerId = newWorker.id;
|
|
172
|
-
const workerDetails = await client.getWorker(newWorker.id);
|
|
173
|
-
logger.info(`Session created for '${this.name}': ${workerDetails.browser_url}`);
|
|
174
|
-
state.sessionId = workerDetails.sessionId;
|
|
175
|
-
await this.refreshSession();
|
|
176
|
-
this.onUpdate();
|
|
177
|
-
})
|
|
178
|
-
.catch((error) => {
|
|
179
|
-
logger.error(`Session start failed '${this.name}'`, error);
|
|
180
|
-
state.started = false;
|
|
181
|
-
this.clearTimers();
|
|
182
|
-
this.onUpdate();
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Deletes the worker.
|
|
187
|
-
* The worker may be created again.
|
|
188
|
-
*/
|
|
189
|
-
terminate() {
|
|
190
|
-
logger.debug(`Terminate session '${this.name}'`);
|
|
191
|
-
return this.tasks.enqueue(async () => {
|
|
192
|
-
const state = this._state;
|
|
193
|
-
if (!state.active)
|
|
194
|
-
return;
|
|
195
|
-
state.active = false;
|
|
196
|
-
this.clearTimers();
|
|
197
|
-
const workerId = state.workerId;
|
|
198
|
-
if (workerId) {
|
|
199
|
-
state.workerId = null;
|
|
200
|
-
try {
|
|
201
|
-
await this.deps.client.deleteWorker(workerId);
|
|
202
|
-
logger.debug(`Terminated worker: ${workerId}`);
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
logger.warn('Could not terminate worker:', error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
logger.debug(`Terminated session '${this.name}'...`);
|
|
209
|
-
this.onUpdate();
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
clearTimers() {
|
|
213
|
-
if (this.pollIntervalId) {
|
|
214
|
-
clearInterval(this.pollIntervalId);
|
|
215
|
-
this.pollIntervalId = null;
|
|
216
|
-
}
|
|
217
|
-
if (this.captureTimeoutId) {
|
|
218
|
-
clearTimeout(this.captureTimeoutId);
|
|
219
|
-
this.captureTimeoutId = null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Deletes the session if it exists.
|
|
224
|
-
*/
|
|
225
|
-
deleteSession() {
|
|
226
|
-
const state = this._state;
|
|
227
|
-
logger.debug(`Delete session '${this.name}'`);
|
|
228
|
-
return this.tasks.enqueue(async () => {
|
|
229
|
-
const sessionId = state.sessionId;
|
|
230
|
-
if (sessionId) {
|
|
231
|
-
state.sessionId = null;
|
|
232
|
-
state.session = null;
|
|
233
|
-
try {
|
|
234
|
-
await this.deps.client.deleteSession(sessionId);
|
|
235
|
-
logger.debug(`Deleted session '${this.name}'`);
|
|
236
|
-
}
|
|
237
|
-
catch (_) {
|
|
238
|
-
logger.warn(`Could not delete session '${this.name}'`);
|
|
239
|
-
}
|
|
240
|
-
this.onUpdate();
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Terminates the worker, deletes the session, starts a new worker.
|
|
246
|
-
*/
|
|
247
|
-
async restart() {
|
|
248
|
-
await this.terminate().catch(() => { });
|
|
249
|
-
await this.deleteSession().catch(() => { });
|
|
250
|
-
await this.start();
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Invoked when the capture timeout has been reached. Checks if data has been received from the REST api.
|
|
254
|
-
*/
|
|
255
|
-
captureTimeoutHandler = () => {
|
|
256
|
-
const state = this._state;
|
|
257
|
-
if (state.totalSpecs == null) {
|
|
258
|
-
if (state.attemptsRemaining > 0) {
|
|
259
|
-
state.attemptsRemaining--;
|
|
260
|
-
logger.warn(`No test results received for ${this.name}, retrying. Attempts remaining: ${state.attemptsRemaining}`);
|
|
261
|
-
this.restart().catch((error) => {
|
|
262
|
-
this.setError(error);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
else if (!this.tryFallbackBrowser()) {
|
|
266
|
-
this.setError(new Error(`Did not capture test progress after ${this.options.retries + 1} attempts for '${this.name}'.`));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
/**
|
|
271
|
-
* If a fallback browser is configured, swaps the worker to it — keeping the
|
|
272
|
-
* rest of the worker options — resets the retry budget, and restarts.
|
|
273
|
-
* Returns true if a fallback was available and a restart was kicked off.
|
|
274
|
-
*/
|
|
275
|
-
tryFallbackBrowser() {
|
|
276
|
-
const state = this._state;
|
|
277
|
-
const { fallbackBrowser } = state.workerOptions;
|
|
278
|
-
if (!fallbackBrowser)
|
|
279
|
-
return false;
|
|
280
|
-
const target = [
|
|
281
|
-
fallbackBrowser.device,
|
|
282
|
-
fallbackBrowser.os,
|
|
283
|
-
fallbackBrowser.os_version,
|
|
284
|
-
fallbackBrowser.browser,
|
|
285
|
-
fallbackBrowser.browser_version,
|
|
286
|
-
]
|
|
287
|
-
.filter(Boolean)
|
|
288
|
-
.join(' ');
|
|
289
|
-
logger.warn(`Did not capture test progress for '${this.name}'; falling back to '${target}'.`);
|
|
290
|
-
// Drop the previous combination's browser fields so a device or
|
|
291
|
-
// browser_version the fallback omits can't bleed through, then apply
|
|
292
|
-
// the fallback over the rest of the options. fallbackBrowser itself is
|
|
293
|
-
// dropped so a repeat failure fails outright rather than looping. The
|
|
294
|
-
// name is relabeled to the fallback platform so its logs and the
|
|
295
|
-
// BrowserStack session reflect the browser actually running.
|
|
296
|
-
const { fallbackBrowser: _fallbackBrowser, browser: _browser, browser_version: _browserVersion, device: _device, ...rest } = state.workerOptions;
|
|
297
|
-
state.workerOptions = {
|
|
298
|
-
...rest,
|
|
299
|
-
...fallbackBrowser,
|
|
300
|
-
name: `${target} [fallback]`,
|
|
301
|
-
};
|
|
302
|
-
state.attemptsRemaining = this.options.retries;
|
|
303
|
-
this.restart().catch((error) => this.setError(error));
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Sets the worker to a failed state and terminates.
|
|
308
|
-
*/
|
|
309
|
-
setError(error) {
|
|
310
|
-
const state = this._state;
|
|
311
|
-
if (state.error)
|
|
312
|
-
return; // Already in an erred state.
|
|
313
|
-
state.error = error;
|
|
314
|
-
logger.error(error == null
|
|
315
|
-
? 'An internal error has occurred.'
|
|
316
|
-
: (error.message ?? String(error)));
|
|
317
|
-
this.tasks
|
|
318
|
-
.enqueue(async () => {
|
|
319
|
-
if (state.sessionId) {
|
|
320
|
-
state.session = await this.deps.client.updateSession(state.sessionId, {
|
|
321
|
-
status: 'failed',
|
|
322
|
-
reason: error?.message ?? 'Internal error',
|
|
323
|
-
});
|
|
324
|
-
this.onUpdate();
|
|
325
|
-
}
|
|
326
|
-
})
|
|
327
|
-
.catch(() => { });
|
|
328
|
-
this.terminate().catch(() => { });
|
|
329
|
-
}
|
|
330
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import type { BrowserStackWorkerOptions, ReadonlyBrowserStackWorkerState } from './BrowserStackWorker';
|
|
2
|
-
import type { BrowserStackAutomateClient, WorkerOptions } from './browserStackRestApi';
|
|
3
|
-
import type { DoneRequest, ProgressRequest, ReportingListener, StartedRequest } from './configureReporterApi';
|
|
4
|
-
export interface BrowserStackWorkerControllerDeps {
|
|
5
|
-
/**
|
|
6
|
-
* The BrowserStack REST API client.
|
|
7
|
-
*/
|
|
8
|
-
readonly client: BrowserStackAutomateClient;
|
|
9
|
-
}
|
|
10
|
-
export interface BrowserStackWorkerControllerOptions {
|
|
11
|
-
/**
|
|
12
|
-
* Configuration to provide to each Worker.
|
|
13
|
-
*/
|
|
14
|
-
readonly worker?: BrowserStackWorkerOptions;
|
|
15
|
-
/**
|
|
16
|
-
* The maximum number of sessions to run in parallel.
|
|
17
|
-
* If not set, the maximum number of sessions according to the automate plan will be used.
|
|
18
|
-
*/
|
|
19
|
-
readonly maxSessions?: number;
|
|
20
|
-
}
|
|
21
|
-
export interface BrowserStackWorkerControllerState {
|
|
22
|
-
readonly workers: readonly ReadonlyBrowserStackWorkerState[];
|
|
23
|
-
/**
|
|
24
|
-
* The number of workers not yet started.
|
|
25
|
-
*/
|
|
26
|
-
pending: number;
|
|
27
|
-
/**
|
|
28
|
-
* The number of workers currently active.
|
|
29
|
-
*/
|
|
30
|
-
running: number;
|
|
31
|
-
/**
|
|
32
|
-
* The number of workers completed and passed.
|
|
33
|
-
*/
|
|
34
|
-
passed: number;
|
|
35
|
-
/**
|
|
36
|
-
* The number of workers completed and failed.
|
|
37
|
-
*/
|
|
38
|
-
failed: number;
|
|
39
|
-
/**
|
|
40
|
-
* The estimated percentage complete, a number from 0-1.
|
|
41
|
-
*/
|
|
42
|
-
percentComplete: number;
|
|
43
|
-
/**
|
|
44
|
-
* The total number of workers not yet completed.
|
|
45
|
-
*/
|
|
46
|
-
remaining: number;
|
|
47
|
-
/**
|
|
48
|
-
* The total number of workers.
|
|
49
|
-
*/
|
|
50
|
-
readonly total: number;
|
|
51
|
-
}
|
|
52
|
-
export type ReadonlyBrowserStackWorkerControllerState = Readonly<BrowserStackWorkerControllerState>;
|
|
53
|
-
export declare class BrowserStackWorkerController implements ReportingListener {
|
|
54
|
-
private readonly deps;
|
|
55
|
-
private readonly options?;
|
|
56
|
-
/**
|
|
57
|
-
* Set to observe state updates.
|
|
58
|
-
*/
|
|
59
|
-
onUpdate: () => void;
|
|
60
|
-
private readonly _state;
|
|
61
|
-
private readonly workers;
|
|
62
|
-
/**
|
|
63
|
-
* Maps the worker's local id to the worker.
|
|
64
|
-
*/
|
|
65
|
-
private readonly localIdToWorkerMap;
|
|
66
|
-
private sessionPollIntervalId;
|
|
67
|
-
/**
|
|
68
|
-
* Tracks an in-flight startSessions invocation. While set, additional calls
|
|
69
|
-
* are deferred until the current one settles to avoid redundant API calls
|
|
70
|
-
* and racing state updates.
|
|
71
|
-
*/
|
|
72
|
-
private startSessionsInFlight;
|
|
73
|
-
/**
|
|
74
|
-
* If set during an in-flight startSessions, indicates another invocation
|
|
75
|
-
* was requested and should run after the current one settles.
|
|
76
|
-
*/
|
|
77
|
-
private startSessionsRequested;
|
|
78
|
-
constructor(deps: BrowserStackWorkerControllerDeps, browsers: readonly WorkerOptions[], options?: BrowserStackWorkerControllerOptions | undefined);
|
|
79
|
-
get state(): ReadonlyBrowserStackWorkerControllerState;
|
|
80
|
-
shouldHandleRequest(id: string): boolean;
|
|
81
|
-
onStarted(request: StartedRequest): void;
|
|
82
|
-
onProgress(request: ProgressRequest): void;
|
|
83
|
-
onDone(request: DoneRequest): void;
|
|
84
|
-
private refreshState;
|
|
85
|
-
/**
|
|
86
|
-
* Refreshes the current state and invokes the onUpdate callback.
|
|
87
|
-
*/
|
|
88
|
-
private updateHandler;
|
|
89
|
-
/**
|
|
90
|
-
* Begins new workers until the maximum parallel sessions is reached.
|
|
91
|
-
* Reentrancy-safe: if invoked while a previous invocation is still
|
|
92
|
-
* running, schedules a single follow-up to run after it settles.
|
|
93
|
-
*/
|
|
94
|
-
private startSessions;
|
|
95
|
-
private clearOpenSessionPollTimeout;
|
|
96
|
-
get allPassed(): boolean;
|
|
97
|
-
start(): void;
|
|
98
|
-
/**
|
|
99
|
-
* Terminates all running workers.
|
|
100
|
-
*/
|
|
101
|
-
terminate(): Promise<void>;
|
|
102
|
-
}
|
|
103
|
-
//# sourceMappingURL=BrowserStackWorkerController.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BrowserStackWorkerController.d.ts","sourceRoot":"","sources":["../../src/browserstack/BrowserStackWorkerController.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,yBAAyB,EACzB,+BAA+B,EAClC,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACR,0BAA0B,EAC1B,aAAa,EAChB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EACR,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACjB,MAAM,wBAAwB,CAAA;AAG/B,MAAM,WAAW,gCAAgC;IAC7C;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAC9C;AAED,MAAM,WAAW,mCAAmC;IAChD;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAA;IAE3C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,iCAAiC;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,+BAA+B,EAAE,CAAA;IAE5D;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,eAAe,EAAE,MAAM,CAAA;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,MAAM,yCAAyC,GACjD,QAAQ,CAAC,iCAAiC,CAAC,CAAA;AAI/C,qBAAa,4BAA6B,YAAW,iBAAiB;IA8B9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IAErB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IA/B7B;;OAEG;IACI,QAAQ,EAAE,MAAM,IAAI,CAAW;IAEtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiC;IAEpE,OAAO,CAAC,qBAAqB,CAAuB;IAEpD;;;;OAIG;IACH,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,OAAO,CAAC,sBAAsB,CAAQ;gBAGjB,IAAI,EAAE,gCAAgC,EACvD,QAAQ,EAAE,SAAS,aAAa,EAAE,EACjB,OAAO,CAAC,EAAE,mCAAmC,YAAA;IA2BlE,IAAI,KAAK,IAAI,yCAAyC,CAErD;IAED,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxC,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAKxC,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAM1C,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAQlC,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,OAAO,CAAC,aAAa,CAMpB;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAwErB,OAAO,CAAC,2BAA2B;IAOnC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,KAAK,IAAI,IAAI;IAKb;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAenC"}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
import { BrowserStackWorker } from './BrowserStackWorker';
|
|
6
|
-
import { logger } from '../util/Logger';
|
|
7
|
-
export class BrowserStackWorkerController {
|
|
8
|
-
deps;
|
|
9
|
-
options;
|
|
10
|
-
/**
|
|
11
|
-
* Set to observe state updates.
|
|
12
|
-
*/
|
|
13
|
-
onUpdate = () => { };
|
|
14
|
-
_state;
|
|
15
|
-
workers;
|
|
16
|
-
/**
|
|
17
|
-
* Maps the worker's local id to the worker.
|
|
18
|
-
*/
|
|
19
|
-
localIdToWorkerMap;
|
|
20
|
-
sessionPollIntervalId = null;
|
|
21
|
-
/**
|
|
22
|
-
* Tracks an in-flight startSessions invocation. While set, additional calls
|
|
23
|
-
* are deferred until the current one settles to avoid redundant API calls
|
|
24
|
-
* and racing state updates.
|
|
25
|
-
*/
|
|
26
|
-
startSessionsInFlight = null;
|
|
27
|
-
/**
|
|
28
|
-
* If set during an in-flight startSessions, indicates another invocation
|
|
29
|
-
* was requested and should run after the current one settles.
|
|
30
|
-
*/
|
|
31
|
-
startSessionsRequested = false;
|
|
32
|
-
constructor(deps, browsers, options) {
|
|
33
|
-
this.deps = deps;
|
|
34
|
-
this.options = options;
|
|
35
|
-
this.workers = browsers.map((browser) => new BrowserStackWorker(deps, browser, options?.worker));
|
|
36
|
-
this._state = {
|
|
37
|
-
workers: this.workers.map((worker) => worker.state),
|
|
38
|
-
pending: this.workers.length,
|
|
39
|
-
remaining: this.workers.length,
|
|
40
|
-
running: 0,
|
|
41
|
-
failed: 0,
|
|
42
|
-
passed: 0,
|
|
43
|
-
percentComplete: 0,
|
|
44
|
-
total: this.workers.length,
|
|
45
|
-
};
|
|
46
|
-
this.localIdToWorkerMap = new Map(this.workers.map((worker) => {
|
|
47
|
-
return [worker.state.localId, worker];
|
|
48
|
-
}));
|
|
49
|
-
// Observe state on all workers
|
|
50
|
-
for (const worker of this.workers) {
|
|
51
|
-
worker.onUpdate = this.updateHandler;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
get state() {
|
|
55
|
-
return this._state;
|
|
56
|
-
}
|
|
57
|
-
shouldHandleRequest(id) {
|
|
58
|
-
return this.localIdToWorkerMap.has(id);
|
|
59
|
-
}
|
|
60
|
-
onStarted(request) {
|
|
61
|
-
const worker = this.localIdToWorkerMap.get(request.id);
|
|
62
|
-
worker.totalSpecs = request.totalSpecs;
|
|
63
|
-
}
|
|
64
|
-
onProgress(request) {
|
|
65
|
-
const worker = this.localIdToWorkerMap.get(request.id);
|
|
66
|
-
worker.totalCompleted = request.totalCompleted;
|
|
67
|
-
worker.appendLogs(request.logs);
|
|
68
|
-
}
|
|
69
|
-
onDone(request) {
|
|
70
|
-
const worker = this.localIdToWorkerMap.get(request.id);
|
|
71
|
-
void worker.done(request.overallStatus === 'passed', request.reason ?? '');
|
|
72
|
-
}
|
|
73
|
-
refreshState() {
|
|
74
|
-
const state = this._state;
|
|
75
|
-
let running = 0;
|
|
76
|
-
let pending = 0;
|
|
77
|
-
let failed = 0;
|
|
78
|
-
let passed = 0;
|
|
79
|
-
let percentSum = 0;
|
|
80
|
-
for (const worker of this.workers) {
|
|
81
|
-
const completed = worker.completed;
|
|
82
|
-
if (completed) {
|
|
83
|
-
if (worker.passed)
|
|
84
|
-
passed++;
|
|
85
|
-
else
|
|
86
|
-
failed++;
|
|
87
|
-
percentSum++;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
if (worker.state.started)
|
|
91
|
-
running++;
|
|
92
|
-
else
|
|
93
|
-
pending++;
|
|
94
|
-
percentSum +=
|
|
95
|
-
worker.state.totalCompleted / (worker.state.totalSpecs || 1);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
state.running = running;
|
|
99
|
-
state.pending = pending;
|
|
100
|
-
state.failed = failed;
|
|
101
|
-
state.passed = passed;
|
|
102
|
-
state.remaining = running + pending;
|
|
103
|
-
state.percentComplete = percentSum / (state.total || 1);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Refreshes the current state and invokes the onUpdate callback.
|
|
107
|
-
*/
|
|
108
|
-
updateHandler = () => {
|
|
109
|
-
const state = this._state;
|
|
110
|
-
const previousRunning = state.running;
|
|
111
|
-
this.refreshState();
|
|
112
|
-
if (previousRunning !== state.running)
|
|
113
|
-
this.startSessions();
|
|
114
|
-
this.onUpdate();
|
|
115
|
-
};
|
|
116
|
-
/**
|
|
117
|
-
* Begins new workers until the maximum parallel sessions is reached.
|
|
118
|
-
* Reentrancy-safe: if invoked while a previous invocation is still
|
|
119
|
-
* running, schedules a single follow-up to run after it settles.
|
|
120
|
-
*/
|
|
121
|
-
startSessions() {
|
|
122
|
-
if (this.startSessionsInFlight) {
|
|
123
|
-
this.startSessionsRequested = true;
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const run = (async () => {
|
|
127
|
-
const state = this._state;
|
|
128
|
-
if (state.pending === 0)
|
|
129
|
-
return;
|
|
130
|
-
const client = this.deps.client;
|
|
131
|
-
const planDetails = await client.getPlan();
|
|
132
|
-
const remainingOpenSessions = Math.min(this.options?.maxSessions ?? Number.MAX_SAFE_INTEGER, planDetails.parallel_sessions_max_allowed) -
|
|
133
|
-
Math.max(state.running, planDetails.parallel_sessions_running);
|
|
134
|
-
logger.debug('Plan parallel sessions:', `${planDetails.parallel_sessions_running} running, ${planDetails.parallel_sessions_max_allowed} allowed`);
|
|
135
|
-
// Takes up to [remainingOpenSessions] pending workers, starts them in parallel, and awaits their startup.
|
|
136
|
-
const workersToStart = this.workers
|
|
137
|
-
.filter((worker) => !worker.state.started)
|
|
138
|
-
.slice(0, remainingOpenSessions);
|
|
139
|
-
if (workersToStart.length) {
|
|
140
|
-
logger.debug(`Starting ${workersToStart.length} sessions...`);
|
|
141
|
-
// Start all workers in parallel
|
|
142
|
-
await Promise.all(workersToStart.map((worker) => worker.start().catch(() => { })));
|
|
143
|
-
}
|
|
144
|
-
this.refreshState();
|
|
145
|
-
if (this._state.pending > 0) {
|
|
146
|
-
// There are pending sessions that were not started, either due
|
|
147
|
-
// no open sessions or a worker start failure.
|
|
148
|
-
// Check if a session opens up after a timeout.
|
|
149
|
-
logger.debug('No open sessions, checking again in 15s.');
|
|
150
|
-
this.clearOpenSessionPollTimeout();
|
|
151
|
-
this.sessionPollIntervalId = setTimeout(() => this.startSessions(), 15000);
|
|
152
|
-
}
|
|
153
|
-
})()
|
|
154
|
-
.catch((error) => {
|
|
155
|
-
logger.warn('startSessions failed', error);
|
|
156
|
-
// Schedule a retry rather than abandoning all pending workers;
|
|
157
|
-
// a transient API failure should not permanently strand them.
|
|
158
|
-
this.clearOpenSessionPollTimeout();
|
|
159
|
-
this.sessionPollIntervalId = setTimeout(() => this.startSessions(), 15000);
|
|
160
|
-
})
|
|
161
|
-
.finally(() => {
|
|
162
|
-
this.startSessionsInFlight = null;
|
|
163
|
-
if (this.startSessionsRequested) {
|
|
164
|
-
this.startSessionsRequested = false;
|
|
165
|
-
// A coalesced request supersedes any retry scheduled by
|
|
166
|
-
// .catch(); otherwise both the immediate run here and the
|
|
167
|
-
// delayed timeout below would fire startSessions().
|
|
168
|
-
this.clearOpenSessionPollTimeout();
|
|
169
|
-
this.startSessions();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
this.startSessionsInFlight = run;
|
|
173
|
-
}
|
|
174
|
-
clearOpenSessionPollTimeout() {
|
|
175
|
-
if (this.sessionPollIntervalId != null) {
|
|
176
|
-
clearTimeout(this.sessionPollIntervalId);
|
|
177
|
-
this.sessionPollIntervalId = null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
get allPassed() {
|
|
181
|
-
return this.workers.every((worker) => worker.passed);
|
|
182
|
-
}
|
|
183
|
-
start() {
|
|
184
|
-
logger.debug(`BrowserStackWorkerController start`);
|
|
185
|
-
this.startSessions();
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Terminates all running workers.
|
|
189
|
-
*/
|
|
190
|
-
async terminate() {
|
|
191
|
-
logger.debug(`Terminating workers`);
|
|
192
|
-
this.clearOpenSessionPollTimeout();
|
|
193
|
-
await Promise.all(this.workers.map((worker) => worker.terminate().catch(() => {
|
|
194
|
-
logger.warn(`Could not terminate worker: '${worker.state.workerOptions.name}'`);
|
|
195
|
-
}))).finally(() => {
|
|
196
|
-
logger.debug('terminate done');
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|