/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {ComponentFixture} from '@angular/core/testing'; import {assertDefined} from 'common/assert_utils'; import {Timestamp} from 'common/time'; import {TimestampConverter} from 'common/timestamp_converter'; import {UrlUtils} from 'common/url_utils'; import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory'; import {TracesParserFactory} from 'parsers/legacy/traces_parser_factory'; import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory'; import {Parser} from 'trace/parser'; import {Trace} from 'trace/trace'; import {Traces} from 'trace/traces'; import {TraceFile} from 'trace/trace_file'; import {TraceEntryTypeMap, TraceType} from 'trace/trace_type'; import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; import {TimestampConverterUtils} from './timestamp_converter_utils'; import {TraceBuilder} from './trace_builder'; class UnitTestUtils { static async getFixtureFile( srcFilename: string, dstFilename: string = srcFilename, ): Promise { const url = UrlUtils.getRootUrl() + 'base/src/test/fixtures/' + srcFilename; const response = await fetch(url); expect(response.ok).toBeTrue(); const blob = await response.blob(); const file = new File([blob], dstFilename); return file; } static async getTrace( type: T, filename: string, ): Promise> { const converter = UnitTestUtils.getTimestampConverter(false); const legacyParsers = await UnitTestUtils.getParsers(filename, converter); expect(legacyParsers.length).toBeLessThanOrEqual(1); if (legacyParsers.length === 1) { expect(legacyParsers[0].getTraceType()).toEqual(type); return new TraceBuilder() .setType(type) .setParser(legacyParsers[0] as unknown as Parser) .build(); } const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename); expect(perfettoParsers.length).toEqual(1); expect(perfettoParsers[0].getTraceType()).toEqual(type); return new TraceBuilder() .setType(type) .setParser(perfettoParsers[0] as unknown as Parser) .build(); } static async getParser( filename: string, converter = UnitTestUtils.getTimestampConverter(), initializeRealToElapsedTimeOffsetNs = true, ): Promise> { const parsers = await UnitTestUtils.getParsers( filename, converter, initializeRealToElapsedTimeOffsetNs, ); return parsers[0]; } static async getParsers( filename: string, converter = UnitTestUtils.getTimestampConverter(), initializeRealToElapsedTimeOffsetNs = true, ): Promise>> { const file = new TraceFile( await UnitTestUtils.getFixtureFile(filename), undefined, ); const fileAndParsers = await new LegacyParserFactory().createParsers( [file], converter, undefined, undefined, ); if (initializeRealToElapsedTimeOffsetNs) { const monotonicOffset = fileAndParsers .find( (fileAndParser) => fileAndParser.parser.getRealToMonotonicTimeOffsetNs() !== undefined, ) ?.parser.getRealToMonotonicTimeOffsetNs(); if (monotonicOffset !== undefined) { converter.setRealToMonotonicTimeOffsetNs(monotonicOffset); } const bootTimeOffset = fileAndParsers .find( (fileAndParser) => fileAndParser.parser.getRealToBootTimeOffsetNs() !== undefined, ) ?.parser.getRealToBootTimeOffsetNs(); if (bootTimeOffset !== undefined) { converter.setRealToBootTimeOffsetNs(bootTimeOffset); } } const parsers = fileAndParsers.map((fileAndParser) => { fileAndParser.parser.createTimestamps(); return fileAndParser.parser; }); expect(parsers.length) .withContext(`Should have been able to create a parser for ${filename}`) .toBeGreaterThanOrEqual(1); return parsers; } static async getPerfettoParser( traceType: T, fixturePath: string, withUTCOffset = false, ): Promise> { const parsers = await UnitTestUtils.getPerfettoParsers( fixturePath, withUTCOffset, ); const parser = assertDefined( parsers.find((parser) => parser.getTraceType() === traceType), ); return parser as Parser; } static async getPerfettoParsers( fixturePath: string, withUTCOffset = false, ): Promise>> { const file = await UnitTestUtils.getFixtureFile(fixturePath); const traceFile = new TraceFile(file); const converter = UnitTestUtils.getTimestampConverter(withUTCOffset); const parsers = await new PerfettoParserFactory().createParsers( traceFile, converter, undefined, ); parsers.forEach((parser) => { converter.setRealToBootTimeOffsetNs( assertDefined(parser.getRealToBootTimeOffsetNs()), ); parser.createTimestamps(); }); return parsers; } static async getTracesParser( filenames: string[], withUTCOffset = false, ): Promise> { const converter = UnitTestUtils.getTimestampConverter(withUTCOffset); const parsersArray = await Promise.all( filenames.map(async (filename) => UnitTestUtils.getParser(filename, converter, true), ), ); const offset = parsersArray .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined) .sort((a, b) => Number( (a.getRealToBootTimeOffsetNs() ?? 0n) - (b.getRealToBootTimeOffsetNs() ?? 0n), ), ) .at(-1) ?.getRealToBootTimeOffsetNs(); if (offset !== undefined) { converter.setRealToBootTimeOffsetNs(offset); } const traces = new Traces(); parsersArray.forEach((parser) => { const trace = Trace.fromParser(parser); traces.addTrace(trace); }); const tracesParsers = await new TracesParserFactory().createParsers( traces, converter, ); expect(tracesParsers.length) .withContext( `Should have been able to create a traces parser for [${filenames.join()}]`, ) .toEqual(1); return tracesParsers[0]; } static getTimestampConverter(withUTCOffset = false): TimestampConverter { return withUTCOffset ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO) : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO); } static async getWindowManagerState(index = 0): Promise { return UnitTestUtils.getTraceEntry( 'traces/elapsed_and_real_timestamp/WindowManager.pb', index, ); } static async getLayerTraceEntry(index = 0): Promise { return await UnitTestUtils.getTraceEntry( 'traces/elapsed_timestamp/SurfaceFlinger.pb', index, ); } static async getViewCaptureEntry(): Promise { return await UnitTestUtils.getTraceEntry( 'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc', ); } static async getMultiDisplayLayerTraceEntry(): Promise { return await UnitTestUtils.getTraceEntry( 'traces/elapsed_and_real_timestamp/SurfaceFlinger_multidisplay.pb', ); } static async getImeTraceEntries(): Promise< [Map, Map] > { let surfaceFlingerEntry: HierarchyTreeNode | undefined; { const parser = (await UnitTestUtils.getParser( 'traces/ime/SurfaceFlinger_with_IME.pb', )) as Parser; surfaceFlingerEntry = await parser.getEntry(5); } let windowManagerEntry: HierarchyTreeNode | undefined; { const parser = (await UnitTestUtils.getParser( 'traces/ime/WindowManager_with_IME.pb', )) as Parser; windowManagerEntry = await parser.getEntry(2); } const entries = new Map(); entries.set( TraceType.INPUT_METHOD_CLIENTS, await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb'), ); entries.set( TraceType.INPUT_METHOD_MANAGER_SERVICE, await UnitTestUtils.getTraceEntry( 'traces/ime/InputMethodManagerService.pb', ), ); entries.set( TraceType.INPUT_METHOD_SERVICE, await UnitTestUtils.getTraceEntry('traces/ime/InputMethodService.pb'), ); entries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry); entries.set(TraceType.WINDOW_MANAGER, windowManagerEntry); const secondEntries = new Map(); secondEntries.set( TraceType.INPUT_METHOD_CLIENTS, await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb', 1), ); secondEntries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry); secondEntries.set(TraceType.WINDOW_MANAGER, windowManagerEntry); return [entries, secondEntries]; } static timestampEqualityTester(first: any, second: any): boolean | undefined { if (first instanceof Timestamp && second instanceof Timestamp) { return UnitTestUtils.testTimestamps(first, second); } return undefined; } static checkSectionCollapseAndExpand( htmlElement: HTMLElement, fixture: ComponentFixture, selector: string, sectionTitle: string, ) { const section = assertDefined(htmlElement.querySelector(selector)); const collapseButton = assertDefined( section.querySelector('collapsible-section-title button'), ) as HTMLElement; collapseButton.click(); fixture.detectChanges(); expect(section.classList).toContain('collapsed'); const collapsedSections = assertDefined( htmlElement.querySelector('collapsed-sections'), ); const collapsedSection = assertDefined( collapsedSections.querySelector('.collapsed-section'), ) as HTMLElement; expect(collapsedSection.textContent).toContain(sectionTitle); collapsedSection.click(); fixture.detectChanges(); UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement); } static checkNoCollapsedSectionButtons(htmlElement: HTMLElement) { const collapsedSections = assertDefined( htmlElement.querySelector('collapsed-sections'), ); expect( collapsedSections.querySelectorAll('.collapsed-section').length, ).toEqual(0); } private static testTimestamps( timestamp: Timestamp, expectedTimestamp: Timestamp, ): boolean { if (timestamp.format() !== expectedTimestamp.format()) return false; if (timestamp.getValueNs() !== expectedTimestamp.getValueNs()) { return false; } return true; } private static async getTraceEntry(filename: string, index = 0) { const parser = (await UnitTestUtils.getParser(filename)) as Parser; return parser.getEntry(index); } } export {UnitTestUtils};