Tests: fix worker restarts for failed browser acknowledgements
[jquery.git] / test / runner / browserstack / browsers.js
blob6489cf9656dadb031882211e7ca95cc41a87f488
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  * }
18  */
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();
39         }
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.` );
49                                 }
50                                 clearTimeout( timeout );
51                                 clearInterval( interval );
52                                 resolve();
53                         }
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.`
63                                 )
64                         );
65                 }, ACKNOWLEDGE_TIMEOUT );
66         } );
69 async function restartWorker( worker ) {
70         await cleanupWorker( worker, worker.options );
71         await createBrowserWorker(
72                 worker.url,
73                 worker.browser,
74                 worker.options,
75                 worker.restarts + 1
76         );
79 export async function restartBrowser( browser ) {
80         const fullBrowser = getBrowserString( browser );
81         const worker = workers[ fullBrowser ];
82         if ( worker ) {
83                 await restartWorker( worker );
84         }
87 async function ensureAcknowledged( worker ) {
88         const fullBrowser = getBrowserString( worker.browser );
89         const verbose = worker.options.verbose;
90         try {
91                 await waitForAck( worker, { fullBrowser, verbose } );
92                 return worker;
93         } catch ( error ) {
94                 console.error( error.message );
95                 await restartWorker( worker );
96         }
99 export async function createBrowserWorker( url, browser, options, restarts = 0 ) {
100         if ( restarts > MAX_WORKER_RESTARTS ) {
101                 throw new Error(
102                         `Reached the maximum number of restarts for ${ chalk.yellow(
103                                 getBrowserString( browser )
104                         ) }`
105                 );
106         }
107         const verbose = options.verbose;
108         while ( ( await getAvailableSessions() ) <= 0 ) {
109                 if ( verbose ) {
110                         console.log( "\nWaiting for available sessions..." );
111                 }
112                 await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
113         }
115         const { debug, runId, tunnelId } = options;
116         const fullBrowser = getBrowserString( browser );
118         const worker = await createWorker( {
119                 ...browser,
120                 url: encodeURI( url ),
121                 project: "jquery",
122                 build: `Run ${ runId }`,
124                 // This is the maximum timeout allowed
125                 // by BrowserStack. We do this because
126                 // we control the timeout in the runner.
127                 // See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
128                 timeout: 1800,
130                 // Not documented in the API docs,
131                 // but required to make local testing work.
132                 // See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
133                 "browserstack.local": true,
134                 "browserstack.localIdentifier": tunnelId
135         } );
137         browser.debug = !!debug;
138         worker.url = url;
139         worker.browser = browser;
140         worker.restarts = restarts;
141         worker.options = options;
142         touchBrowser( browser );
143         workers[ fullBrowser ] = worker;
145         // Wait for the worker to show up in the list
146         // before returning it.
147         return ensureAcknowledged( worker );
150 export async function setBrowserWorkerUrl( browser, url ) {
151         const fullBrowser = getBrowserString( browser );
152         const worker = workers[ fullBrowser ];
153         if ( worker ) {
154                 worker.url = url;
155         }
159  * Checks that all browsers have received
160  * a response in the given amount of time.
161  * If not, the worker is restarted.
162  */
163 export async function checkLastTouches() {
164         for ( const [ fullBrowser, worker ] of Object.entries( workers ) ) {
165                 if ( Date.now() - worker.lastTouch > RUN_WORKER_TIMEOUT ) {
166                         const options = worker.options;
167                         if ( options.verbose ) {
168                                 console.log(
169                                         `\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
170                                                 RUN_WORKER_TIMEOUT / 1000 / 60
171                                         }min.`
172                                 );
173                         }
174                         await restartWorker( worker );
175                 }
176         }
179 export async function cleanupWorker( worker, { verbose } ) {
180         for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
181                 if ( w === worker ) {
182                         delete workers[ fullBrowser ];
183                         await deleteWorker( worker.id );
184                         if ( verbose ) {
185                                 console.log( `\nStopped ${ fullBrowser }.` );
186                         }
187                         return;
188                 }
189         }
192 export async function cleanupAllBrowsers( { verbose } ) {
193         const workersRemaining = Object.values( workers );
194         const numRemaining = workersRemaining.length;
195         if ( numRemaining ) {
196                 try {
197                         await Promise.all(
198                                 workersRemaining.map( ( worker ) => deleteWorker( worker.id ) )
199                         );
200                         if ( verbose ) {
201                                 console.log(
202                                         `Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
203                                 );
204                         }
205                 } catch ( error ) {
207                         // Log the error, but do not consider the test run failed
208                         console.error( error );
209                 }
210         }