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