Tests: fix flakey message logs; ignore delete worker failures
[jquery.git] / test / runner / browserstack / browsers.js
blob3a7da4fc9f0da459adf15788f435ced7b5b58b7f
1 import chalk from "chalk";
2 import { getBrowserString } from "../lib/getBrowserString.js";
3 import { createWorker, deleteWorker, getAvailableSessions } from "./api.js";
5 const workers = Object.create( null );
7 /**
8 * Keys are browser strings
9 * Structure of a worker:
10 * {
11 * debug: boolean, // Stops the worker from being cleaned up when finished
12 * id: string,
13 * lastTouch: number, // The last time a request was received
14 * url: string,
15 * browser: object, // The browser object
16 * options: object // The options to create the worker
17 * }
20 // Acknowledge the worker within the time limit.
21 // BrowserStack can take much longer spinning up
22 // some browsers, such as iOS 15 Safari.
23 const ACKNOWLEDGE_INTERVAL = 1000;
24 const ACKNOWLEDGE_TIMEOUT = 60 * 1000 * 5;
26 const MAX_WORKER_RESTARTS = 5;
28 // No report after the time limit
29 // should refresh the worker
30 const RUN_WORKER_TIMEOUT = 60 * 1000 * 2;
32 const WORKER_WAIT_TIME = 30000;
34 export function touchBrowser( browser ) {
35 const fullBrowser = getBrowserString( browser );
36 const worker = workers[ fullBrowser ];
37 if ( worker ) {
38 worker.lastTouch = Date.now();
42 async function waitForAck( worker, { fullBrowser, verbose } ) {
43 delete worker.lastTouch;
44 return new Promise( ( resolve, reject ) => {
45 const interval = setInterval( () => {
46 if ( worker.lastTouch ) {
47 if ( verbose ) {
48 console.log( `\n${ fullBrowser } acknowledged.` );
50 clearTimeout( timeout );
51 clearInterval( interval );
52 resolve();
54 }, ACKNOWLEDGE_INTERVAL );
56 const timeout = setTimeout( () => {
57 clearInterval( interval );
58 reject(
59 new Error(
60 `${ fullBrowser } not acknowledged after ${
61 ACKNOWLEDGE_TIMEOUT / 1000 / 60
62 }min.`
65 }, ACKNOWLEDGE_TIMEOUT );
66 } );
69 async function ensureAcknowledged( worker, restarts ) {
70 const fullBrowser = getBrowserString( worker.browser );
71 const verbose = worker.options.verbose;
72 try {
73 await waitForAck( worker, { fullBrowser, verbose } );
74 return worker;
75 } catch ( error ) {
76 console.error( error.message );
77 await cleanupWorker( worker, { verbose } );
78 await createBrowserWorker(
79 worker.url,
80 worker.browser,
81 worker.options,
82 restarts + 1
87 export async function createBrowserWorker( url, browser, options, restarts = 0 ) {
88 if ( restarts > MAX_WORKER_RESTARTS ) {
89 throw new Error(
90 `Reached the maximum number of restarts for ${ chalk.yellow(
91 getBrowserString( browser )
92 ) }`
95 const verbose = options.verbose;
96 while ( ( await getAvailableSessions() ) <= 0 ) {
97 if ( verbose ) {
98 console.log( "\nWaiting for available sessions..." );
100 await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
103 const { debug, runId, tunnelId } = options;
104 const fullBrowser = getBrowserString( browser );
106 const worker = await createWorker( {
107 ...browser,
108 url: encodeURI( url ),
109 project: "jquery",
110 build: `Run ${ runId }`,
112 // This is the maximum timeout allowed
113 // by BrowserStack. We do this because
114 // we control the timeout in the runner.
115 // See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
116 timeout: 1800,
118 // Not documented in the API docs,
119 // but required to make local testing work.
120 // See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
121 "browserstack.local": true,
122 "browserstack.localIdentifier": tunnelId
123 } );
125 browser.debug = !!debug;
126 worker.url = url;
127 worker.browser = browser;
128 worker.restarts = restarts;
129 worker.options = options;
130 touchBrowser( browser );
131 workers[ fullBrowser ] = worker;
133 // Wait for the worker to show up in the list
134 // before returning it.
135 return ensureAcknowledged( worker, restarts );
138 export async function setBrowserWorkerUrl( browser, url ) {
139 const fullBrowser = getBrowserString( browser );
140 const worker = workers[ fullBrowser ];
141 if ( worker ) {
142 worker.url = url;
147 * Checks that all browsers have received
148 * a response in the given amount of time.
149 * If not, the worker is restarted.
151 export async function checkLastTouches() {
152 for ( const [ fullBrowser, worker ] of Object.entries( workers ) ) {
153 if ( Date.now() - worker.lastTouch > RUN_WORKER_TIMEOUT ) {
154 const options = worker.options;
155 if ( options.verbose ) {
156 console.log(
157 `\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
158 RUN_WORKER_TIMEOUT / 1000 / 60
159 }min.`
162 await cleanupWorker( worker, options );
163 await createBrowserWorker(
164 worker.url,
165 worker.browser,
166 options,
167 worker.restarts
173 export async function cleanupWorker( worker, { verbose } ) {
174 for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
175 if ( w === worker ) {
176 delete workers[ fullBrowser ];
177 await deleteWorker( worker.id );
178 if ( verbose ) {
179 console.log( `\nStopped ${ fullBrowser }.` );
181 return;
186 export async function cleanupAllBrowsers( { verbose } ) {
187 const workersRemaining = Object.values( workers );
188 const numRemaining = workersRemaining.length;
189 if ( numRemaining ) {
190 try {
191 await Promise.all(
192 workersRemaining.map( ( worker ) => deleteWorker( worker.id ) )
194 if ( verbose ) {
195 console.log(
196 `Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
199 } catch ( error ) {
201 // Log the error, but do not consider the test run failed
202 console.error( error );