• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {ComponentFixture} from '@angular/core/testing';
18import {assertDefined, assertTrue} from 'common/assert_utils';
19import {TimestampConverterUtils} from 'common/time/test_utils';
20import {Timestamp} from 'common/time/time';
21import {TimestampConverter} from 'common/time/timestamp_converter';
22import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory';
23import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
24import {TracesParserFactory} from 'parsers/traces/traces_parser_factory';
25import {Parser} from 'trace/parser';
26import {Trace} from 'trace/trace';
27import {Traces} from 'trace/traces';
28import {TraceFile} from 'trace/trace_file';
29import {TraceMetadata} from 'trace/trace_metadata';
30import {TraceEntryTypeMap, TraceType} from 'trace/trace_type';
31import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
32import {
33  ColumnType,
34  QueryResult,
35  Row,
36  RowIterator,
37} from 'trace_processor/query_result';
38import {TraceProcessorFactory} from 'trace_processor/trace_processor_factory';
39import {getFixtureFile} from './fixture_utils';
40import {TraceBuilder} from './trace_builder';
41
42class UnitTestUtils {
43  static async getTrace<T extends TraceType>(
44    type: T,
45    filename: string,
46  ): Promise<Trace<T>> {
47    const converter = UnitTestUtils.getTimestampConverter(false);
48    const legacyParsers = await UnitTestUtils.getParsers(filename, converter);
49    expect(legacyParsers.length).toBeLessThanOrEqual(1);
50    if (legacyParsers.length === 1) {
51      expect(legacyParsers[0].getTraceType()).toEqual(type);
52      return new TraceBuilder<T>()
53        .setType(type)
54        .setParser(legacyParsers[0] as unknown as Parser<T>)
55        .build();
56    }
57
58    const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename);
59    expect(perfettoParsers.length).toEqual(1);
60    expect(perfettoParsers[0].getTraceType()).toEqual(type);
61    return new TraceBuilder<T>()
62      .setType(type)
63      .setParser(perfettoParsers[0] as unknown as Parser<T>)
64      .build();
65  }
66
67  static async getParser(
68    filename: string,
69    converter = UnitTestUtils.getTimestampConverter(),
70    initializeRealToElapsedTimeOffsetNs = true,
71    metadata: TraceMetadata = {},
72  ): Promise<Parser<object>> {
73    const parsers = await UnitTestUtils.getParsers(
74      filename,
75      converter,
76      initializeRealToElapsedTimeOffsetNs,
77      metadata,
78    );
79
80    expect(parsers.length)
81      .withContext(`Should have been able to create a parser for ${filename}`)
82      .toBeGreaterThanOrEqual(1);
83
84    return parsers[0];
85  }
86
87  static async getParsers(
88    filename: string,
89    converter = UnitTestUtils.getTimestampConverter(),
90    initializeRealToElapsedTimeOffsetNs = true,
91    metadata: TraceMetadata = {},
92  ): Promise<Array<Parser<object>>> {
93    const file = new TraceFile(await getFixtureFile(filename), undefined);
94    const fileAndParsers = await new LegacyParserFactory().createParsers(
95      [file],
96      converter,
97      metadata,
98    );
99
100    if (initializeRealToElapsedTimeOffsetNs) {
101      const monotonicOffset = fileAndParsers
102        .find(
103          (fileAndParser) =>
104            fileAndParser.parser.getRealToMonotonicTimeOffsetNs() !== undefined,
105        )
106        ?.parser.getRealToMonotonicTimeOffsetNs();
107      if (monotonicOffset !== undefined) {
108        converter.setRealToMonotonicTimeOffsetNs(monotonicOffset);
109      }
110      const bootTimeOffset = fileAndParsers
111        .find(
112          (fileAndParser) =>
113            fileAndParser.parser.getRealToBootTimeOffsetNs() !== undefined,
114        )
115        ?.parser.getRealToBootTimeOffsetNs();
116      if (bootTimeOffset !== undefined) {
117        converter.setRealToBootTimeOffsetNs(bootTimeOffset);
118      }
119    }
120
121    return fileAndParsers.map((fileAndParser) => {
122      fileAndParser.parser.createTimestamps();
123      return fileAndParser.parser;
124    });
125  }
126
127  static async getPerfettoParser<T extends TraceType>(
128    traceType: T,
129    fixturePath: string,
130    withUTCOffset = false,
131  ): Promise<Parser<TraceEntryTypeMap[T]>> {
132    const parsers = await UnitTestUtils.getPerfettoParsers(
133      fixturePath,
134      withUTCOffset,
135    );
136    const parser = assertDefined(
137      parsers.find((parser) => parser.getTraceType() === traceType),
138    );
139    return parser as Parser<TraceEntryTypeMap[T]>;
140  }
141
142  static async getPerfettoParsers(
143    fixturePath: string,
144    withUTCOffset = false,
145  ): Promise<Array<Parser<object>>> {
146    const file = await getFixtureFile(fixturePath);
147    const traceFile = new TraceFile(file);
148    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
149    const parsers = await new PerfettoParserFactory().createParsers(
150      traceFile,
151      converter,
152      undefined,
153    );
154    parsers.forEach((parser) => {
155      converter.setRealToBootTimeOffsetNs(
156        assertDefined(parser.getRealToBootTimeOffsetNs()),
157      );
158      parser.createTimestamps();
159    });
160    return parsers;
161  }
162
163  static async getTracesParser(
164    filenames: string[],
165    withUTCOffset = false,
166  ): Promise<Parser<object>> {
167    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
168    const legacyParsers = (
169      await Promise.all(
170        filenames.map(async (filename) =>
171          UnitTestUtils.getParsers(filename, converter, true),
172        ),
173      )
174    ).reduce((acc, cur) => acc.concat(cur), []);
175
176    const perfettoParsers = (
177      await Promise.all(
178        filenames.map(async (filename) =>
179          UnitTestUtils.getPerfettoParsers(filename),
180        ),
181      )
182    ).reduce((acc, cur) => acc.concat(cur), []);
183
184    const parsersArray = legacyParsers.concat(perfettoParsers);
185
186    const offset = parsersArray
187      .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined)
188      .sort((a, b) =>
189        Number(
190          (a.getRealToBootTimeOffsetNs() ?? 0n) -
191            (b.getRealToBootTimeOffsetNs() ?? 0n),
192        ),
193      )
194      .at(-1)
195      ?.getRealToBootTimeOffsetNs();
196
197    if (offset !== undefined) {
198      converter.setRealToBootTimeOffsetNs(offset);
199    }
200
201    const traces = new Traces();
202    parsersArray.forEach((parser) => {
203      const trace = Trace.fromParser(parser);
204      traces.addTrace(trace);
205    });
206
207    const tracesParsers = await new TracesParserFactory().createParsers(
208      traces,
209      converter,
210    );
211    assertTrue(
212      tracesParsers.length === 1,
213      () =>
214        `Should have been able to create a traces parser for [${filenames.join()}]`,
215    );
216    return tracesParsers[0];
217  }
218
219  static getTimestampConverter(withUTCOffset = false): TimestampConverter {
220    return withUTCOffset
221      ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO)
222      : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO);
223  }
224
225  static async getWindowManagerState(index = 0): Promise<HierarchyTreeNode> {
226    return UnitTestUtils.getTraceEntry(
227      'traces/elapsed_and_real_timestamp/WindowManager.pb',
228      index,
229    );
230  }
231
232  static async getLayerTraceEntry(index = 0): Promise<HierarchyTreeNode> {
233    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
234      'traces/elapsed_timestamp/SurfaceFlinger.pb',
235      index,
236    );
237  }
238
239  static async getViewCaptureEntry(): Promise<HierarchyTreeNode> {
240    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
241      'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc',
242    );
243  }
244
245  static async getMultiDisplayLayerTraceEntry(): Promise<HierarchyTreeNode> {
246    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
247      'traces/elapsed_and_real_timestamp/SurfaceFlinger_multidisplay.pb',
248    );
249  }
250
251  static async getImeTraceEntries(): Promise<
252    [Map<TraceType, HierarchyTreeNode>, Map<TraceType, HierarchyTreeNode>]
253  > {
254    let surfaceFlingerEntry: HierarchyTreeNode | undefined;
255    {
256      const parser = (await UnitTestUtils.getParser(
257        'traces/ime/SurfaceFlinger_with_IME.pb',
258      )) as Parser<HierarchyTreeNode>;
259      surfaceFlingerEntry = await parser.getEntry(5);
260    }
261
262    let windowManagerEntry: HierarchyTreeNode | undefined;
263    {
264      const parser = (await UnitTestUtils.getParser(
265        'traces/ime/WindowManager_with_IME.pb',
266      )) as Parser<HierarchyTreeNode>;
267      windowManagerEntry = await parser.getEntry(2);
268    }
269
270    const entries = new Map<TraceType, HierarchyTreeNode>();
271    entries.set(
272      TraceType.INPUT_METHOD_CLIENTS,
273      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb'),
274    );
275    entries.set(
276      TraceType.INPUT_METHOD_MANAGER_SERVICE,
277      await UnitTestUtils.getTraceEntry(
278        'traces/ime/InputMethodManagerService.pb',
279      ),
280    );
281    entries.set(
282      TraceType.INPUT_METHOD_SERVICE,
283      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodService.pb'),
284    );
285    entries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry);
286    entries.set(TraceType.WINDOW_MANAGER, windowManagerEntry);
287
288    const secondEntries = new Map<TraceType, HierarchyTreeNode>();
289    secondEntries.set(
290      TraceType.INPUT_METHOD_CLIENTS,
291      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb', 1),
292    );
293    secondEntries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry);
294    secondEntries.set(TraceType.WINDOW_MANAGER, windowManagerEntry);
295
296    return [entries, secondEntries];
297  }
298
299  static async getTraceEntry<T>(filename: string, index = 0) {
300    const parser = (await UnitTestUtils.getParser(filename)) as Parser<T>;
301    return parser.getEntry(index);
302  }
303
304  static checkSectionCollapseAndExpand<T>(
305    htmlElement: HTMLElement,
306    fixture: ComponentFixture<T>,
307    selector: string,
308    sectionTitle: string,
309  ) {
310    const section = assertDefined(htmlElement.querySelector(selector));
311    expect(
312      assertDefined(
313        section.querySelector<HTMLElement>(
314          'collapsible-section-title .mat-title',
315        ),
316      ).textContent,
317    ).toEqual(sectionTitle);
318    const collapseButton = assertDefined(
319      section.querySelector<HTMLElement>('collapsible-section-title button'),
320    );
321    collapseButton.click();
322    fixture.detectChanges();
323    expect(section.classList).toContain('collapsed');
324    const collapsedSections = assertDefined(
325      htmlElement.querySelector('collapsed-sections'),
326    );
327    const collapsedSection = assertDefined(
328      collapsedSections.querySelector('.collapsed-section'),
329    ) as HTMLElement;
330    expect(collapsedSection.textContent?.trim()).toEqual(
331      sectionTitle + '  arrow_right',
332    );
333    collapsedSection.click();
334    fixture.detectChanges();
335    UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement);
336  }
337
338  static checkNoCollapsedSectionButtons(htmlElement: HTMLElement) {
339    const collapsedSections = assertDefined(
340      htmlElement.querySelector('collapsed-sections'),
341    );
342    expect(
343      collapsedSections.querySelectorAll('.collapsed-section').length,
344    ).toEqual(0);
345  }
346
347  static makeEmptyTrace<T extends TraceType>(
348    traceType: T,
349    descriptors: string[] = [],
350  ): Trace<TraceEntryTypeMap[T]> {
351    return new TraceBuilder<TraceEntryTypeMap[T]>()
352      .setEntries([])
353      .setTimestamps([])
354      .setDescriptors(descriptors)
355      .setType(traceType)
356      .build();
357  }
358
359  static makeSearchTraceSpies(
360    ts?: Timestamp,
361    value?: ColumnType,
362  ): [jasmine.SpyObj<QueryResult>, jasmine.SpyObj<RowIterator<Row>>] {
363    const spyQueryResult = jasmine.createSpyObj<QueryResult>('result', [
364      'numRows',
365      'columns',
366      'iter',
367    ]);
368    spyQueryResult.numRows.and.returnValue(1);
369    const columns: string[] = [];
370    if (ts !== undefined) columns.push('ts');
371    columns.push('property');
372    if (value !== undefined) columns.push('value');
373    spyQueryResult.columns.and.returnValue(columns);
374
375    const spyIter = jasmine.createSpyObj<RowIterator<Row>>('iter', [
376      'valid',
377      'next',
378      'get',
379    ]);
380    if (ts !== undefined) {
381      spyIter.get.withArgs('ts').and.returnValue(ts.getValueNs());
382    }
383    spyIter.get.withArgs('property').and.returnValue('test_property');
384    if (value !== undefined) {
385      spyIter.get.withArgs('value').and.returnValue(value);
386    }
387    spyIter.valid.and.returnValue(true);
388    spyIter.next.and.callFake(() =>
389      assertDefined(spyIter).valid.and.returnValue(false),
390    );
391    spyQueryResult.iter.and.returnValue(spyIter);
392
393    return [spyQueryResult, spyIter];
394  }
395
396  static async runQueryAndGetResult(query: string): Promise<QueryResult> {
397    const tp = await TraceProcessorFactory.getSingleInstance();
398    return tp.queryAllRows(query);
399  }
400
401  static async checkTooltips<T>(
402    elements: Element[],
403    expTooltips: Array<string | undefined>,
404    fixture: ComponentFixture<T>,
405  ) {
406    for (const [index, el] of elements.entries()) {
407      el.dispatchEvent(new Event('mouseenter'));
408      fixture.detectChanges();
409      const panel = document.querySelector<HTMLElement>('.mat-tooltip-panel');
410      if (expTooltips[index] !== undefined) {
411        expect(panel?.textContent).toEqual(expTooltips[index]);
412      } else {
413        expect(panel).toBeNull();
414      }
415      el.dispatchEvent(new Event('mouseleave'));
416      fixture.detectChanges();
417      await fixture.whenStable();
418    }
419  }
420
421  static makeFakeWebSocket(): jasmine.SpyObj<WebSocket> {
422    const socket = jasmine.createSpyObj<WebSocket>(
423      'WebSocket',
424      ['onmessage', 'onclose', 'send', 'close', 'onerror'],
425      {'readyState': WebSocket.OPEN, binaryType: 'arraybuffer'},
426    );
427    socket.close.and.callFake(() => {
428      socket.onclose!(new CloseEvent(''));
429    });
430    return socket;
431  }
432
433  static makeFakeWebSocketMessage(
434    data: Blob | ArrayBuffer | number | string,
435  ): MessageEvent {
436    return jasmine.createSpyObj<MessageEvent>([], {'data': data});
437  }
438}
439
440export {UnitTestUtils};
441