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