diff --git a/js/Gruntfile.js b/js/Gruntfile.js index 4f827dd..36dd9c0 100644 --- a/js/Gruntfile.js +++ b/js/Gruntfile.js @@ -9,8 +9,8 @@ const Gruntfile = require( '../../chipper/js/grunt/Gruntfile' ); const ContinuousServer = require( './server/ContinuousServer' ); +const ContinuousServerClient = require( './server/ContinuousServerClient' ); const assert = require( 'assert' ); -const grunt = require( 'grunt' ); // eslint-disable-line const _ = require( 'lodash' ); // eslint-disable-line const winston = require( 'winston' ); const QuickServer = require( './server/QuickServer' ); @@ -79,4 +79,20 @@ module.exports = grunt => { server.startMainLoop(); } ); + + grunt.registerTask( + 'client-server', + 'Launches puppeteer clients to run tests for CT with the following options:\n' + + '--clients=NUMBER : specify how many puppeteer clients to run with, defaults to 16\n', + () => { + + // We don't finish! Don't tell grunt this... + grunt.task.current.async(); + + const server = new ContinuousServerClient( { + numberOfPuppeteers: grunt.option( 'clients' ) ? grunt.option( 'clients' ) : 16 + } ); + server.startMainLoop(); + } + ); }; diff --git a/js/server/ContinuousServerClient.js b/js/server/ContinuousServerClient.js new file mode 100644 index 0000000..820f692 --- /dev/null +++ b/js/server/ContinuousServerClient.js @@ -0,0 +1,75 @@ +// Copyright 2022, University of Colorado Boulder + +/** + * A Node script that handles multiple browser clients for Continuous Testing's server. This file uses Workers to kick + * off instances of Puppeteer that will load the continuous-loop. This file is hard coded to point to bayes via https, + * and will need to be updated if that URL is no longer correct. + * + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +const _ = require( 'lodash' ); // eslint-disable-line require-statement-match +const path = require( 'path' ); +const assert = require( 'assert' ); +const { Worker } = require( 'worker_threads' ); // eslint-disable-line require-statement-match +const sleep = require( '../../../perennial/js/common/sleep' ); + +process.on( 'SIGINT', () => process.exit( 0 ) ); + +class ContinuousServerClient { + constructor( options ) { + + options = { + rootDir: path.normalize( `${__dirname}/../../../` ), + numberOfPuppeteers: 16, + ...options + }; + + // @public {string} - root of your GitHub working copy, relative to the name of the directory that the + // currently-executing script resides in + this.rootDir = options.rootDir; + + this.numberOfPuppeteers = options.numberOfPuppeteers; + } + + /** + * Kick off a worker, add it to a list, and when complete, remove it from that list + * @private + * @param {Worker[]} workerList + * @returns {Promise} + */ + newClientWorker( workerList ) { + //first argument is filename of the worker + const worker = new Worker( `${this.rootDir}/aqua/js/server/puppeteerCTClient.js`, { argv: [ 'Bayes%20Puppeteer' ] } ); + workerList.push( worker ); + worker.on( 'message', message => { console.log( 'Message from puppeteerClient:', message ); } ); + worker.on( 'error', e => { console.error( 'Error from puppeteerClient:', e ); } ); + worker.on( 'exit', code => { + const index = _.indexOf( workerList, worker ); + assert( index !== -1, 'worker must be in list' ); + workerList.splice( index, 1 ); + if ( code !== 0 ) { + console.error( `Worker stopped with exit code ${code}` ); + } + } ); + } + + /** + * @public + */ + async startMainLoop() { + + const workers = []; + + while ( true ) { // eslint-disable-line + while ( workers.length < this.numberOfPuppeteers ) { + this.newClientWorker( workers ); + } + + // Check back in every 30 seconds to see if we need to restart any workers. + await sleep( 30000 ); + } + } +} + +module.exports = ContinuousServerClient; diff --git a/js/server/puppeteerCTClient.js b/js/server/puppeteerCTClient.js new file mode 100644 index 0000000..a9802b8 --- /dev/null +++ b/js/server/puppeteerCTClient.js @@ -0,0 +1,32 @@ +// Copyright 2022, University of Colorado Boulder + +/** + * Launch puppeteer and point it to CT running on bayes for 15 minutes. + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +const assert = require( 'assert' ); +const puppeteerLoad = require( '../../../perennial/js/common/puppeteerLoad' ); +const { parentPort } = require( 'worker_threads' ); // eslint-disable-line require-statement-match + +process.on( 'SIGINT', () => process.exit() ); + +( async () => { + + assert( process.argv[ 2 ], 'usage: node puppeteerHelpCT {{SOME_IDENTIFIER_HERE}}' ); + const url = `https://bayes.colorado.edu/continuous-testing/aqua/html/continuous-loop.html?id=${process.argv[ 2 ]}`; + const error = await puppeteerLoad( url, { + waitAfterLoad: 15 * 60 * 1000, // 15 minutes + allowedTimeToLoad: 120000, + puppeteerTimeout: 1000000000, + + // A page error is what we are testing for. Don't fail the browser instance out when an assertion occurs + resolvePageErrors: false + } ); + if ( error ) { + parentPort.postMessage( error ); + } + + // The worker didn't seem to exit without this line + process.exit( 0 ); +} )();