• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2021 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import fs from 'fs';
16import net from 'net';
17import path from 'path';
18import pixelmatch from 'pixelmatch';
19import {PNG} from 'pngjs';
20import {Page} from 'puppeteer';
21
22// These constants have been hand selected by comparing the diffs of screenshots
23// between Linux on Mac. Unfortunately font-rendering is platform-specific.
24// Even though we force the same antialiasing and hinting settings, some minimal
25// differences exist.
26const DIFF_PER_PIXEL_THRESHOLD = 0.35;
27const DIFF_MAX_PIXELS = 50;
28
29// Waits for the Perfetto UI to be quiescent, using a union of heuristics:
30// - Check that the progress bar is not animating.
31// - Check that the omnibox is not showing a message.
32// - Check that no redraws are pending in our RAF scheduler.
33// - Check that all the above is satisfied for |minIdleMs| consecutive ms.
34export async function waitForPerfettoIdle(page: Page, minIdleMs?: number) {
35  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
36  minIdleMs = minIdleMs || 3000;
37  const tickMs = 250;
38  const timeoutMs = 60000;
39  const minIdleTicks = Math.ceil(minIdleMs / tickMs);
40  const timeoutTicks = Math.ceil(timeoutMs / tickMs);
41  let consecutiveIdleTicks = 0;
42  let reasons: string[] = [];
43  for (let ticks = 0; ticks < timeoutTicks; ticks++) {
44    await new Promise((r) => setTimeout(r, tickMs));
45    const isShowingMsg = !!(await page.$('.omnibox.message-mode'));
46    const isShowingAnim = !!(await page.$('.progress.progress-anim'));
47    const hasPendingRedraws = await (
48      await page.evaluateHandle('raf.hasPendingRedraws')
49    ).jsonValue();
50
51    if (isShowingAnim || isShowingMsg || hasPendingRedraws) {
52      consecutiveIdleTicks = 0;
53      reasons = [];
54      if (isShowingAnim) {
55        reasons.push('showing progress animation');
56      }
57      if (isShowingMsg) {
58        reasons.push('showing omnibox message');
59      }
60      if (hasPendingRedraws) {
61        reasons.push('has pending redraws');
62      }
63      continue;
64    }
65    if (++consecutiveIdleTicks >= minIdleTicks) {
66      return;
67    }
68  }
69  throw new Error(
70    `waitForPerfettoIdle() failed. Did not reach idle after ${timeoutMs} ms. ` +
71      `Reasons not considered idle: ${reasons.join(', ')}`,
72  );
73}
74
75export function getTestTracePath(fname: string): string {
76  const fPath = path.join('test', 'data', fname);
77  if (!fs.existsSync(fPath)) {
78    throw new Error('Could not locate trace file ' + fPath);
79  }
80  return fPath;
81}
82
83export async function compareScreenshots(
84  reportPath: string,
85  actualFilename: string,
86  expectedFilename: string,
87) {
88  if (!fs.existsSync(expectedFilename)) {
89    throw new Error(
90      `Could not find ${expectedFilename}. Run wih REBASELINE=1.`,
91    );
92  }
93  const actualImg = PNG.sync.read(fs.readFileSync(actualFilename));
94  const expectedImg = PNG.sync.read(fs.readFileSync(expectedFilename));
95  const {width, height} = actualImg;
96  expect(width).toEqual(expectedImg.width);
97  expect(height).toEqual(expectedImg.height);
98  const diffPng = new PNG({width, height});
99  const diff = await pixelmatch(
100    actualImg.data,
101    expectedImg.data,
102    diffPng.data,
103    width,
104    height,
105    {
106      threshold: DIFF_PER_PIXEL_THRESHOLD,
107    },
108  );
109  if (diff > DIFF_MAX_PIXELS) {
110    const diffFilename = actualFilename.replace('.png', '-diff.png');
111    fs.writeFileSync(diffFilename, PNG.sync.write(diffPng));
112    fs.appendFileSync(
113      reportPath,
114      `${path.basename(actualFilename)};${path.basename(diffFilename)}\n`,
115    );
116    fail(`Diff test failed on ${diffFilename}, delta: ${diff} pixels`);
117  }
118  return diff;
119}
120
121// If the user has a trace_processor_shell --httpd instance open, bail out,
122// as that will invalidate the test loading different data.
123export async function failIfTraceProcessorHttpdIsActive() {
124  return new Promise<void>((resolve, reject) => {
125    const client = new net.Socket();
126    client.connect(9001, '127.0.0.1', () => {
127      const err =
128        'trace_processor_shell --httpd detected on port 9001. ' +
129        'Bailing out as it interferes with the tests. ' +
130        'Please kill that and run the test again.';
131      console.error(err);
132      client.destroy();
133      reject(err);
134    });
135    client.on('error', (e: {code: string}) => {
136      expect(e.code).toBe('ECONNREFUSED');
137      resolve();
138    });
139    client.end();
140  });
141}
142