/* * 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, assertTrue} from 'common/assert_utils'; import {TimestampConverterUtils} from 'common/time/test_utils'; import {Timestamp} from 'common/time/time'; import {TimestampConverter} from 'common/time/timestamp_converter'; import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory'; import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory'; import {TracesParserFactory} from 'parsers/traces/traces_parser_factory'; import {Parser} from 'trace/parser'; import {Trace} from 'trace/trace'; import {Traces} from 'trace/traces'; import {TraceFile} from 'trace/trace_file'; import {TraceMetadata} from 'trace/trace_metadata'; import {TraceEntryTypeMap, TraceType} from 'trace/trace_type'; import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; import { ColumnType, QueryResult, Row, RowIterator, } from 'trace_processor/query_result'; import {TraceProcessorFactory} from 'trace_processor/trace_processor_factory'; import {getFixtureFile} from './fixture_utils'; import {TraceBuilder} from './trace_builder'; class UnitTestUtils { 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, metadata: TraceMetadata = {}, ): Promise> { const parsers = await UnitTestUtils.getParsers( filename, converter, initializeRealToElapsedTimeOffsetNs, metadata, ); expect(parsers.length) .withContext(`Should have been able to create a parser for ${filename}`) .toBeGreaterThanOrEqual(1); return parsers[0]; } static async getParsers( filename: string, converter = UnitTestUtils.getTimestampConverter(), initializeRealToElapsedTimeOffsetNs = true, metadata: TraceMetadata = {}, ): Promise>> { const file = new TraceFile(await getFixtureFile(filename), undefined); const fileAndParsers = await new LegacyParserFactory().createParsers( [file], converter, metadata, ); 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); } } return fileAndParsers.map((fileAndParser) => { fileAndParser.parser.createTimestamps(); return fileAndParser.parser; }); } 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 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 legacyParsers = ( await Promise.all( filenames.map(async (filename) => UnitTestUtils.getParsers(filename, converter, true), ), ) ).reduce((acc, cur) => acc.concat(cur), []); const perfettoParsers = ( await Promise.all( filenames.map(async (filename) => UnitTestUtils.getPerfettoParsers(filename), ), ) ).reduce((acc, cur) => acc.concat(cur), []); const parsersArray = legacyParsers.concat(perfettoParsers); 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, ); assertTrue( tracesParsers.length === 1, () => `Should have been able to create a traces parser for [${filenames.join()}]`, ); 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 async getTraceEntry(filename: string, index = 0) { const parser = (await UnitTestUtils.getParser(filename)) as Parser; return parser.getEntry(index); } static checkSectionCollapseAndExpand( htmlElement: HTMLElement, fixture: ComponentFixture, selector: string, sectionTitle: string, ) { const section = assertDefined(htmlElement.querySelector(selector)); expect( assertDefined( section.querySelector( 'collapsible-section-title .mat-title', ), ).textContent, ).toEqual(sectionTitle); const collapseButton = assertDefined( section.querySelector('collapsible-section-title button'), ); 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?.trim()).toEqual( sectionTitle + ' arrow_right', ); 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); } static makeEmptyTrace( traceType: T, descriptors: string[] = [], ): Trace { return new TraceBuilder() .setEntries([]) .setTimestamps([]) .setDescriptors(descriptors) .setType(traceType) .build(); } static makeSearchTraceSpies( ts?: Timestamp, value?: ColumnType, ): [jasmine.SpyObj, jasmine.SpyObj>] { const spyQueryResult = jasmine.createSpyObj('result', [ 'numRows', 'columns', 'iter', ]); spyQueryResult.numRows.and.returnValue(1); const columns: string[] = []; if (ts !== undefined) columns.push('ts'); columns.push('property'); if (value !== undefined) columns.push('value'); spyQueryResult.columns.and.returnValue(columns); const spyIter = jasmine.createSpyObj>('iter', [ 'valid', 'next', 'get', ]); if (ts !== undefined) { spyIter.get.withArgs('ts').and.returnValue(ts.getValueNs()); } spyIter.get.withArgs('property').and.returnValue('test_property'); if (value !== undefined) { spyIter.get.withArgs('value').and.returnValue(value); } spyIter.valid.and.returnValue(true); spyIter.next.and.callFake(() => assertDefined(spyIter).valid.and.returnValue(false), ); spyQueryResult.iter.and.returnValue(spyIter); return [spyQueryResult, spyIter]; } static async runQueryAndGetResult(query: string): Promise { const tp = await TraceProcessorFactory.getSingleInstance(); return tp.queryAllRows(query); } static async checkTooltips( elements: Element[], expTooltips: Array, fixture: ComponentFixture, ) { for (const [index, el] of elements.entries()) { el.dispatchEvent(new Event('mouseenter')); fixture.detectChanges(); const panel = document.querySelector('.mat-tooltip-panel'); if (expTooltips[index] !== undefined) { expect(panel?.textContent).toEqual(expTooltips[index]); } else { expect(panel).toBeNull(); } el.dispatchEvent(new Event('mouseleave')); fixture.detectChanges(); await fixture.whenStable(); } } static makeFakeWebSocket(): jasmine.SpyObj { const socket = jasmine.createSpyObj( 'WebSocket', ['onmessage', 'onclose', 'send', 'close', 'onerror'], {'readyState': WebSocket.OPEN, binaryType: 'arraybuffer'}, ); socket.close.and.callFake(() => { socket.onclose!(new CloseEvent('')); }); return socket; } static makeFakeWebSocketMessage( data: Blob | ArrayBuffer | number | string, ): MessageEvent { return jasmine.createSpyObj([], {'data': data}); } } export {UnitTestUtils};