1cb93a386Sopenharmony_ci/** 2cb93a386Sopenharmony_ci * Command line application to test GMS and unit tests with puppeteer. 3cb93a386Sopenharmony_ci * node run-wasm-gm-tests --js_file ../../out/wasm_gm_tests/wasm_gm_tests.js --wasm_file ../../out/wasm_gm_tests/wasm_gm_tests.wasm --known_hashes /tmp/gold2/tests/hashes.txt --output /tmp/gold2/tests/ --use_gpu --timeout 180 4cb93a386Sopenharmony_ci */ 5cb93a386Sopenharmony_ciconst puppeteer = require('puppeteer'); 6cb93a386Sopenharmony_ciconst express = require('express'); 7cb93a386Sopenharmony_ciconst path = require('path'); 8cb93a386Sopenharmony_ciconst bodyParser = require('body-parser'); 9cb93a386Sopenharmony_ciconst fs = require('fs'); 10cb93a386Sopenharmony_ciconst commandLineArgs = require('command-line-args'); 11cb93a386Sopenharmony_ciconst commandLineUsage = require('command-line-usage'); 12cb93a386Sopenharmony_ci 13cb93a386Sopenharmony_ciconst opts = [ 14cb93a386Sopenharmony_ci { 15cb93a386Sopenharmony_ci name: 'js_file', 16cb93a386Sopenharmony_ci typeLabel: '{underline file}', 17cb93a386Sopenharmony_ci description: '(required) The path to wasm_gm_tests.js.' 18cb93a386Sopenharmony_ci }, 19cb93a386Sopenharmony_ci { 20cb93a386Sopenharmony_ci name: 'wasm_file', 21cb93a386Sopenharmony_ci typeLabel: '{underline file}', 22cb93a386Sopenharmony_ci description: '(required) The path to wasm_gm_tests.wasm.' 23cb93a386Sopenharmony_ci }, 24cb93a386Sopenharmony_ci { 25cb93a386Sopenharmony_ci name: 'known_hashes', 26cb93a386Sopenharmony_ci typeLabel: '{underline file}', 27cb93a386Sopenharmony_ci description: '(required) The hashes that should not be written to disk.' 28cb93a386Sopenharmony_ci }, 29cb93a386Sopenharmony_ci { 30cb93a386Sopenharmony_ci name: 'output', 31cb93a386Sopenharmony_ci typeLabel: '{underline file}', 32cb93a386Sopenharmony_ci description: '(required) The directory to write the output JSON and images to.', 33cb93a386Sopenharmony_ci }, 34cb93a386Sopenharmony_ci { 35cb93a386Sopenharmony_ci name: 'resources', 36cb93a386Sopenharmony_ci typeLabel: '{underline file}', 37cb93a386Sopenharmony_ci description: '(required) The directory that test images are stored in.', 38cb93a386Sopenharmony_ci }, 39cb93a386Sopenharmony_ci { 40cb93a386Sopenharmony_ci name: 'use_gpu', 41cb93a386Sopenharmony_ci description: 'Whether we should run in non-headless mode with GPU.', 42cb93a386Sopenharmony_ci type: Boolean, 43cb93a386Sopenharmony_ci }, 44cb93a386Sopenharmony_ci { 45cb93a386Sopenharmony_ci name: 'enable_simd', 46cb93a386Sopenharmony_ci description: 'enable execution of wasm SIMD operations in chromium', 47cb93a386Sopenharmony_ci type: Boolean 48cb93a386Sopenharmony_ci }, 49cb93a386Sopenharmony_ci { 50cb93a386Sopenharmony_ci name: 'port', 51cb93a386Sopenharmony_ci description: 'The port number to use, defaults to 8081.', 52cb93a386Sopenharmony_ci type: Number, 53cb93a386Sopenharmony_ci }, 54cb93a386Sopenharmony_ci { 55cb93a386Sopenharmony_ci name: 'help', 56cb93a386Sopenharmony_ci alias: 'h', 57cb93a386Sopenharmony_ci type: Boolean, 58cb93a386Sopenharmony_ci description: 'Print this usage guide.' 59cb93a386Sopenharmony_ci }, 60cb93a386Sopenharmony_ci { 61cb93a386Sopenharmony_ci name: 'timeout', 62cb93a386Sopenharmony_ci description: 'Number of seconds to allow test to run.', 63cb93a386Sopenharmony_ci type: Number, 64cb93a386Sopenharmony_ci }, 65cb93a386Sopenharmony_ci { 66cb93a386Sopenharmony_ci name: 'manual_mode', 67cb93a386Sopenharmony_ci description: 'If set, tests will not run automatically.', 68cb93a386Sopenharmony_ci type: Boolean, 69cb93a386Sopenharmony_ci }, 70cb93a386Sopenharmony_ci { 71cb93a386Sopenharmony_ci name: 'batch_size', 72cb93a386Sopenharmony_ci description: 'Number of gms (or unit tests) to run in a batch. The main thread ' + 73cb93a386Sopenharmony_ci 'of the page is only unlocked between batches. Default: 50. Use 1 for debugging.', 74cb93a386Sopenharmony_ci type: Number, 75cb93a386Sopenharmony_ci } 76cb93a386Sopenharmony_ci]; 77cb93a386Sopenharmony_ci 78cb93a386Sopenharmony_ciconst usage = [ 79cb93a386Sopenharmony_ci { 80cb93a386Sopenharmony_ci header: 'Measuring correctness of Skia WASM code', 81cb93a386Sopenharmony_ci content: 'Command line application to capture images drawn from tests', 82cb93a386Sopenharmony_ci }, 83cb93a386Sopenharmony_ci { 84cb93a386Sopenharmony_ci header: 'Options', 85cb93a386Sopenharmony_ci optionList: opts, 86cb93a386Sopenharmony_ci }, 87cb93a386Sopenharmony_ci]; 88cb93a386Sopenharmony_ci 89cb93a386Sopenharmony_ci// Parse and validate flags. 90cb93a386Sopenharmony_ciconst options = commandLineArgs(opts); 91cb93a386Sopenharmony_ci 92cb93a386Sopenharmony_ciif (!options.port) { 93cb93a386Sopenharmony_ci options.port = 8081; 94cb93a386Sopenharmony_ci} 95cb93a386Sopenharmony_ciif (!options.timeout) { 96cb93a386Sopenharmony_ci options.timeout = 60; 97cb93a386Sopenharmony_ci} 98cb93a386Sopenharmony_ciif (!options.batch_size) { 99cb93a386Sopenharmony_ci options.batch_size = 50; 100cb93a386Sopenharmony_ci} 101cb93a386Sopenharmony_ci 102cb93a386Sopenharmony_ciif (options.help) { 103cb93a386Sopenharmony_ci console.log(commandLineUsage(usage)); 104cb93a386Sopenharmony_ci process.exit(0); 105cb93a386Sopenharmony_ci} 106cb93a386Sopenharmony_ci 107cb93a386Sopenharmony_ciif (!options.output) { 108cb93a386Sopenharmony_ci console.error('You must supply an output directory.'); 109cb93a386Sopenharmony_ci console.log(commandLineUsage(usage)); 110cb93a386Sopenharmony_ci process.exit(1); 111cb93a386Sopenharmony_ci} 112cb93a386Sopenharmony_ci 113cb93a386Sopenharmony_ciif (!options.js_file) { 114cb93a386Sopenharmony_ci console.error('You must supply path to wasm_gm_tests.js.'); 115cb93a386Sopenharmony_ci console.log(commandLineUsage(usage)); 116cb93a386Sopenharmony_ci process.exit(1); 117cb93a386Sopenharmony_ci} 118cb93a386Sopenharmony_ci 119cb93a386Sopenharmony_ciif (!options.wasm_file) { 120cb93a386Sopenharmony_ci console.error('You must supply path to wasm_gm_tests.wasm.'); 121cb93a386Sopenharmony_ci console.log(commandLineUsage(usage)); 122cb93a386Sopenharmony_ci process.exit(1); 123cb93a386Sopenharmony_ci} 124cb93a386Sopenharmony_ci 125cb93a386Sopenharmony_ciif (!options.known_hashes) { 126cb93a386Sopenharmony_ci console.error('You must supply path to known_hashes.txt'); 127cb93a386Sopenharmony_ci console.log(commandLineUsage(usage)); 128cb93a386Sopenharmony_ci process.exit(1); 129cb93a386Sopenharmony_ci} 130cb93a386Sopenharmony_ci 131cb93a386Sopenharmony_ciif (!options.resources) { 132cb93a386Sopenharmony_ci console.error('You must supply resources directory'); 133cb93a386Sopenharmony_ci console.log(commandLineUsage(usage)); 134cb93a386Sopenharmony_ci process.exit(1); 135cb93a386Sopenharmony_ci} 136cb93a386Sopenharmony_ci 137cb93a386Sopenharmony_ciconst resourceBaseDir = path.resolve(options.resources) 138cb93a386Sopenharmony_ci// This executes recursively and synchronously. 139cb93a386Sopenharmony_ciconst recursivelyListFiles = (dir) => { 140cb93a386Sopenharmony_ci const absolutePaths = []; 141cb93a386Sopenharmony_ci const files = fs.readdirSync(dir); 142cb93a386Sopenharmony_ci files.forEach((file) => { 143cb93a386Sopenharmony_ci const filepath = path.join(dir, file); 144cb93a386Sopenharmony_ci const stats = fs.statSync(filepath); 145cb93a386Sopenharmony_ci if (stats.isDirectory()) { 146cb93a386Sopenharmony_ci absolutePaths.push(...recursivelyListFiles(filepath)); 147cb93a386Sopenharmony_ci } else if (stats.isFile()) { 148cb93a386Sopenharmony_ci absolutePaths.push(path.relative(resourceBaseDir, filepath)); 149cb93a386Sopenharmony_ci } 150cb93a386Sopenharmony_ci }); 151cb93a386Sopenharmony_ci return absolutePaths; 152cb93a386Sopenharmony_ci}; 153cb93a386Sopenharmony_ci 154cb93a386Sopenharmony_ciconst resourceListing = recursivelyListFiles(options.resources); 155cb93a386Sopenharmony_ciconsole.log('Saw resources', resourceListing); 156cb93a386Sopenharmony_ci 157cb93a386Sopenharmony_ciconst driverHTML = fs.readFileSync('run-wasm-gm-tests.html', 'utf8'); 158cb93a386Sopenharmony_ciconst testJS = fs.readFileSync(options.js_file, 'utf8'); 159cb93a386Sopenharmony_ciconst testWASM = fs.readFileSync(options.wasm_file, 'binary'); 160cb93a386Sopenharmony_ciconst knownHashes = fs.readFileSync(options.known_hashes, 'utf8'); 161cb93a386Sopenharmony_ci 162cb93a386Sopenharmony_ci// This express webserver will serve the HTML file running the benchmark and any additional assets 163cb93a386Sopenharmony_ci// needed to run the tests. 164cb93a386Sopenharmony_ciconst app = express(); 165cb93a386Sopenharmony_ciapp.get('/', (req, res) => res.send(driverHTML)); 166cb93a386Sopenharmony_ci 167cb93a386Sopenharmony_ciapp.use('/static/resources/', express.static(resourceBaseDir)); 168cb93a386Sopenharmony_ciconsole.log('resources served from', resourceBaseDir); 169cb93a386Sopenharmony_ci 170cb93a386Sopenharmony_ci// This allows the server to receive POST requests of up to 10MB for image/png and read the body 171cb93a386Sopenharmony_ci// as raw bytes, housed in a buffer. 172cb93a386Sopenharmony_ciapp.use(bodyParser.raw({ type: 'image/png', limit: '10mb' })); 173cb93a386Sopenharmony_ci 174cb93a386Sopenharmony_ciapp.get('/static/hashes.txt', (req, res) => res.send(knownHashes)); 175cb93a386Sopenharmony_ciapp.get('/static/resource_listing.json', (req, res) => res.send(JSON.stringify(resourceListing))); 176cb93a386Sopenharmony_ciapp.get('/static/wasm_gm_tests.js', (req, res) => res.send(testJS)); 177cb93a386Sopenharmony_ciapp.get('/static/wasm_gm_tests.wasm', function(req, res) { 178cb93a386Sopenharmony_ci // Set the MIME type so it can be streamed efficiently. 179cb93a386Sopenharmony_ci res.type('application/wasm'); 180cb93a386Sopenharmony_ci res.send(new Buffer(testWASM, 'binary')); 181cb93a386Sopenharmony_ci}); 182cb93a386Sopenharmony_ciapp.post('/write_png', (req, res) => { 183cb93a386Sopenharmony_ci const md5 = req.header('X-MD5-Hash'); 184cb93a386Sopenharmony_ci if (!md5) { 185cb93a386Sopenharmony_ci res.sendStatus(400); 186cb93a386Sopenharmony_ci return; 187cb93a386Sopenharmony_ci } 188cb93a386Sopenharmony_ci const data = req.body; 189cb93a386Sopenharmony_ci const newFile = path.join(options.output, md5 + '.png'); 190cb93a386Sopenharmony_ci fs.writeFileSync(newFile, data, { 191cb93a386Sopenharmony_ci encoding: 'binary', 192cb93a386Sopenharmony_ci }); 193cb93a386Sopenharmony_ci res.sendStatus(200); 194cb93a386Sopenharmony_ci}); 195cb93a386Sopenharmony_ci 196cb93a386Sopenharmony_ciconst server = app.listen(options.port, () => console.log('- Local web server started.')); 197cb93a386Sopenharmony_ci 198cb93a386Sopenharmony_ciconst hash = options.use_gpu? '#gpu': '#cpu'; 199cb93a386Sopenharmony_ciconst targetURL = `http://localhost:${options.port}/${hash}`; 200cb93a386Sopenharmony_ciconst viewPort = {width: 1000, height: 1000}; 201cb93a386Sopenharmony_ci 202cb93a386Sopenharmony_ci// Drive chrome to load the web page from the server we have running. 203cb93a386Sopenharmony_ciasync function driveBrowser() { 204cb93a386Sopenharmony_ci console.log('- Launching chrome for ' + options.input); 205cb93a386Sopenharmony_ci const browser_args = [ 206cb93a386Sopenharmony_ci '--no-sandbox', 207cb93a386Sopenharmony_ci '--disable-setuid-sandbox', 208cb93a386Sopenharmony_ci '--window-size=' + viewPort.width + ',' + viewPort.height, 209cb93a386Sopenharmony_ci // The following two params allow Chrome to run at an unlimited fps. Note, if there is 210cb93a386Sopenharmony_ci // already a chrome instance running, these arguments will have NO EFFECT, as the existing 211cb93a386Sopenharmony_ci // Chrome instance will be used instead of puppeteer spinning up a new one. 212cb93a386Sopenharmony_ci '--disable-frame-rate-limit', 213cb93a386Sopenharmony_ci '--disable-gpu-vsync', 214cb93a386Sopenharmony_ci ]; 215cb93a386Sopenharmony_ci if (options.enable_simd) { 216cb93a386Sopenharmony_ci browser_args.push('--enable-features=WebAssemblySimd'); 217cb93a386Sopenharmony_ci } 218cb93a386Sopenharmony_ci if (options.use_gpu) { 219cb93a386Sopenharmony_ci browser_args.push('--ignore-gpu-blacklist'); 220cb93a386Sopenharmony_ci browser_args.push('--ignore-gpu-blocklist'); 221cb93a386Sopenharmony_ci browser_args.push('--enable-gpu-rasterization'); 222cb93a386Sopenharmony_ci } 223cb93a386Sopenharmony_ci const headless = !options.use_gpu; 224cb93a386Sopenharmony_ci console.log("Running with headless: " + headless + " args: " + browser_args); 225cb93a386Sopenharmony_ci let browser; 226cb93a386Sopenharmony_ci let page; 227cb93a386Sopenharmony_ci try { 228cb93a386Sopenharmony_ci browser = await puppeteer.launch({ 229cb93a386Sopenharmony_ci headless: headless, 230cb93a386Sopenharmony_ci args: browser_args, 231cb93a386Sopenharmony_ci executablePath: options.chromium_executable_path 232cb93a386Sopenharmony_ci }); 233cb93a386Sopenharmony_ci page = await browser.newPage(); 234cb93a386Sopenharmony_ci await page.setViewport(viewPort); 235cb93a386Sopenharmony_ci } catch (e) { 236cb93a386Sopenharmony_ci console.log('Could not open the browser.', e); 237cb93a386Sopenharmony_ci process.exit(1); 238cb93a386Sopenharmony_ci } 239cb93a386Sopenharmony_ci console.log("Loading " + targetURL); 240cb93a386Sopenharmony_ci let failed = []; 241cb93a386Sopenharmony_ci try { 242cb93a386Sopenharmony_ci await page.goto(targetURL, { 243cb93a386Sopenharmony_ci timeout: 60000, 244cb93a386Sopenharmony_ci waitUntil: 'networkidle0' 245cb93a386Sopenharmony_ci }); 246cb93a386Sopenharmony_ci 247cb93a386Sopenharmony_ci if (options.manual_mode) { 248cb93a386Sopenharmony_ci console.log('Manual mode detected. Will hang'); 249cb93a386Sopenharmony_ci // Wait a very long time, with the web server running. 250cb93a386Sopenharmony_ci await page.waitForFunction(`window._abort_manual_mode`, { 251cb93a386Sopenharmony_ci timeout: 1000000000, 252cb93a386Sopenharmony_ci polling: 1000, 253cb93a386Sopenharmony_ci }); 254cb93a386Sopenharmony_ci } 255cb93a386Sopenharmony_ci 256cb93a386Sopenharmony_ci // Page is mostly loaded, wait for test harness page to report itself ready. Some resources 257cb93a386Sopenharmony_ci // may still be loading. 258cb93a386Sopenharmony_ci console.log('Waiting 30s for test harness to be ready'); 259cb93a386Sopenharmony_ci await page.waitForFunction(`(window._testsReady === true) || window._error`, { 260cb93a386Sopenharmony_ci timeout: 30000, 261cb93a386Sopenharmony_ci }); 262cb93a386Sopenharmony_ci 263cb93a386Sopenharmony_ci const err = await page.evaluate('window._error'); 264cb93a386Sopenharmony_ci if (err) { 265cb93a386Sopenharmony_ci const log = await page.evaluate('window._log'); 266cb93a386Sopenharmony_ci console.info(log); 267cb93a386Sopenharmony_ci console.error(`ERROR: ${err}`); 268cb93a386Sopenharmony_ci process.exit(1); 269cb93a386Sopenharmony_ci } 270cb93a386Sopenharmony_ci 271cb93a386Sopenharmony_ci // There is a button with id #start_tests to click (this also makes manual debugging easier). 272cb93a386Sopenharmony_ci await page.click('#start_tests'); 273cb93a386Sopenharmony_ci 274cb93a386Sopenharmony_ci // Rather than wait a long time for things to finish, we send progress updates every 50 tests. 275cb93a386Sopenharmony_ci let batch = options.batch_size; 276cb93a386Sopenharmony_ci while (true) { 277cb93a386Sopenharmony_ci console.log(`Waiting ${options.timeout}s for ${options.batch_size} tests to complete`); 278cb93a386Sopenharmony_ci await page.waitForFunction(`(window._testsProgress >= ${batch}) || window._testsDone || window._error`, { 279cb93a386Sopenharmony_ci timeout: options.timeout*1000, 280cb93a386Sopenharmony_ci }); 281cb93a386Sopenharmony_ci const progress = await page.evaluate(() => { 282cb93a386Sopenharmony_ci return { 283cb93a386Sopenharmony_ci err: window._error, 284cb93a386Sopenharmony_ci done: window._testsDone, 285cb93a386Sopenharmony_ci count: window._testsProgress, 286cb93a386Sopenharmony_ci }; 287cb93a386Sopenharmony_ci }); 288cb93a386Sopenharmony_ci if (progress.err) { 289cb93a386Sopenharmony_ci const log = await page.evaluate('window._log'); 290cb93a386Sopenharmony_ci console.info(log); 291cb93a386Sopenharmony_ci console.error(`ERROR: ${progress.err}`); 292cb93a386Sopenharmony_ci process.exit(1); 293cb93a386Sopenharmony_ci } 294cb93a386Sopenharmony_ci if (progress.done) { 295cb93a386Sopenharmony_ci console.log(`Completed ${progress.count} tests. Finished.`); 296cb93a386Sopenharmony_ci break; 297cb93a386Sopenharmony_ci } 298cb93a386Sopenharmony_ci console.log(`In Progress; completed ${progress.count} tests.`) 299cb93a386Sopenharmony_ci batch = progress.count + options.batch_size; 300cb93a386Sopenharmony_ci } 301cb93a386Sopenharmony_ci const goldResults = await page.evaluate('window._results'); 302cb93a386Sopenharmony_ci failed = await(page.evaluate('window._failed')); 303cb93a386Sopenharmony_ci 304cb93a386Sopenharmony_ci const log = await page.evaluate('window._log'); 305cb93a386Sopenharmony_ci console.info(log); 306cb93a386Sopenharmony_ci 307cb93a386Sopenharmony_ci 308cb93a386Sopenharmony_ci const jsonFile = path.join(options.output, 'gold_results.json'); 309cb93a386Sopenharmony_ci fs.writeFileSync(jsonFile, JSON.stringify(goldResults)); 310cb93a386Sopenharmony_ci } catch(e) { 311cb93a386Sopenharmony_ci console.log('Timed out while loading, drawing, or writing to disk.', e); 312cb93a386Sopenharmony_ci if (page) { 313cb93a386Sopenharmony_ci const log = await page.evaluate('window._log'); 314cb93a386Sopenharmony_ci console.error(log); 315cb93a386Sopenharmony_ci } 316cb93a386Sopenharmony_ci await browser.close(); 317cb93a386Sopenharmony_ci await new Promise((resolve) => server.close(resolve)); 318cb93a386Sopenharmony_ci process.exit(1); 319cb93a386Sopenharmony_ci } 320cb93a386Sopenharmony_ci 321cb93a386Sopenharmony_ci await browser.close(); 322cb93a386Sopenharmony_ci await new Promise((resolve) => server.close(resolve)); 323cb93a386Sopenharmony_ci 324cb93a386Sopenharmony_ci if (failed.length > 0) { 325cb93a386Sopenharmony_ci console.error('Failed tests', failed); 326cb93a386Sopenharmony_ci process.exit(1); 327cb93a386Sopenharmony_ci } else { 328cb93a386Sopenharmony_ci process.exit(0); 329cb93a386Sopenharmony_ci } 330cb93a386Sopenharmony_ci} 331cb93a386Sopenharmony_ci 332cb93a386Sopenharmony_cidriveBrowser(); 333