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