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} from 'common/assert_utils';
19import {Timestamp} from 'common/time';
20import {TimestampConverter} from 'common/timestamp_converter';
21import {UrlUtils} from 'common/url_utils';
22import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory';
23import {TracesParserFactory} from 'parsers/legacy/traces_parser_factory';
24import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
25import {Parser} from 'trace/parser';
26import {Trace} from 'trace/trace';
27import {Traces} from 'trace/traces';
28import {TraceFile} from 'trace/trace_file';
29import {TraceEntryTypeMap, TraceType} from 'trace/trace_type';
30import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
31import {TimestampConverterUtils} from './timestamp_converter_utils';
32import {TraceBuilder} from './trace_builder';
33
34class UnitTestUtils {
35  static async getFixtureFile(
36    srcFilename: string,
37    dstFilename: string = srcFilename,
38  ): Promise<File> {
39    const url = UrlUtils.getRootUrl() + 'base/src/test/fixtures/' + srcFilename;
40    const response = await fetch(url);
41    expect(response.ok).toBeTrue();
42    const blob = await response.blob();
43    const file = new File([blob], dstFilename);
44    return file;
45  }
46
47  static async getTrace<T extends TraceType>(
48    type: T,
49    filename: string,
50  ): Promise<Trace<T>> {
51    const converter = UnitTestUtils.getTimestampConverter(false);
52    const legacyParsers = await UnitTestUtils.getParsers(filename, converter);
53    expect(legacyParsers.length).toBeLessThanOrEqual(1);
54    if (legacyParsers.length === 1) {
55      expect(legacyParsers[0].getTraceType()).toEqual(type);
56      return new TraceBuilder<T>()
57        .setType(type)
58        .setParser(legacyParsers[0] as unknown as Parser<T>)
59        .build();
60    }
61
62    const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename);
63    expect(perfettoParsers.length).toEqual(1);
64    expect(perfettoParsers[0].getTraceType()).toEqual(type);
65    return new TraceBuilder<T>()
66      .setType(type)
67      .setParser(perfettoParsers[0] as unknown as Parser<T>)
68      .build();
69  }
70
71  static async getParser(
72    filename: string,
73    converter = UnitTestUtils.getTimestampConverter(),
74    initializeRealToElapsedTimeOffsetNs = true,
75  ): Promise<Parser<object>> {
76    const parsers = await UnitTestUtils.getParsers(
77      filename,
78      converter,
79      initializeRealToElapsedTimeOffsetNs,
80    );
81    return parsers[0];
82  }
83
84  static async getParsers(
85    filename: string,
86    converter = UnitTestUtils.getTimestampConverter(),
87    initializeRealToElapsedTimeOffsetNs = true,
88  ): Promise<Array<Parser<object>>> {
89    const file = new TraceFile(
90      await UnitTestUtils.getFixtureFile(filename),
91      undefined,
92    );
93    const fileAndParsers = await new LegacyParserFactory().createParsers(
94      [file],
95      converter,
96      undefined,
97      undefined,
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    const parsers = fileAndParsers.map((fileAndParser) => {
122      fileAndParser.parser.createTimestamps();
123      return fileAndParser.parser;
124    });
125
126    expect(parsers.length)
127      .withContext(`Should have been able to create a parser for ${filename}`)
128      .toBeGreaterThanOrEqual(1);
129
130    return parsers;
131  }
132
133  static async getPerfettoParser<T extends TraceType>(
134    traceType: T,
135    fixturePath: string,
136    withUTCOffset = false,
137  ): Promise<Parser<TraceEntryTypeMap[T]>> {
138    const parsers = await UnitTestUtils.getPerfettoParsers(
139      fixturePath,
140      withUTCOffset,
141    );
142    const parser = assertDefined(
143      parsers.find((parser) => parser.getTraceType() === traceType),
144    );
145    return parser as Parser<TraceEntryTypeMap[T]>;
146  }
147
148  static async getPerfettoParsers(
149    fixturePath: string,
150    withUTCOffset = false,
151  ): Promise<Array<Parser<object>>> {
152    const file = await UnitTestUtils.getFixtureFile(fixturePath);
153    const traceFile = new TraceFile(file);
154    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
155    const parsers = await new PerfettoParserFactory().createParsers(
156      traceFile,
157      converter,
158      undefined,
159    );
160    parsers.forEach((parser) => {
161      converter.setRealToBootTimeOffsetNs(
162        assertDefined(parser.getRealToBootTimeOffsetNs()),
163      );
164      parser.createTimestamps();
165    });
166    return parsers;
167  }
168
169  static async getTracesParser(
170    filenames: string[],
171    withUTCOffset = false,
172  ): Promise<Parser<object>> {
173    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
174    const parsersArray = await Promise.all(
175      filenames.map(async (filename) =>
176        UnitTestUtils.getParser(filename, converter, true),
177      ),
178    );
179    const offset = parsersArray
180      .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined)
181      .sort((a, b) =>
182        Number(
183          (a.getRealToBootTimeOffsetNs() ?? 0n) -
184            (b.getRealToBootTimeOffsetNs() ?? 0n),
185        ),
186      )
187      .at(-1)
188      ?.getRealToBootTimeOffsetNs();
189
190    if (offset !== undefined) {
191      converter.setRealToBootTimeOffsetNs(offset);
192    }
193
194    const traces = new Traces();
195    parsersArray.forEach((parser) => {
196      const trace = Trace.fromParser(parser);
197      traces.addTrace(trace);
198    });
199
200    const tracesParsers = await new TracesParserFactory().createParsers(
201      traces,
202      converter,
203    );
204    expect(tracesParsers.length)
205      .withContext(
206        `Should have been able to create a traces parser for [${filenames.join()}]`,
207      )
208      .toEqual(1);
209    return tracesParsers[0];
210  }
211
212  static getTimestampConverter(withUTCOffset = false): TimestampConverter {
213    return withUTCOffset
214      ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO)
215      : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO);
216  }
217
218  static async getWindowManagerState(index = 0): Promise<HierarchyTreeNode> {
219    return UnitTestUtils.getTraceEntry(
220      'traces/elapsed_and_real_timestamp/WindowManager.pb',
221      index,
222    );
223  }
224
225  static async getLayerTraceEntry(index = 0): Promise<HierarchyTreeNode> {
226    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
227      'traces/elapsed_timestamp/SurfaceFlinger.pb',
228      index,
229    );
230  }
231
232  static async getViewCaptureEntry(): Promise<HierarchyTreeNode> {
233    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
234      'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc',
235    );
236  }
237
238  static async getMultiDisplayLayerTraceEntry(): Promise<HierarchyTreeNode> {
239    return await UnitTestUtils.getTraceEntry<HierarchyTreeNode>(
240      'traces/elapsed_and_real_timestamp/SurfaceFlinger_multidisplay.pb',
241    );
242  }
243
244  static async getImeTraceEntries(): Promise<
245    [Map<TraceType, HierarchyTreeNode>, Map<TraceType, HierarchyTreeNode>]
246  > {
247    let surfaceFlingerEntry: HierarchyTreeNode | undefined;
248    {
249      const parser = (await UnitTestUtils.getParser(
250        'traces/ime/SurfaceFlinger_with_IME.pb',
251      )) as Parser<HierarchyTreeNode>;
252      surfaceFlingerEntry = await parser.getEntry(5);
253    }
254
255    let windowManagerEntry: HierarchyTreeNode | undefined;
256    {
257      const parser = (await UnitTestUtils.getParser(
258        'traces/ime/WindowManager_with_IME.pb',
259      )) as Parser<HierarchyTreeNode>;
260      windowManagerEntry = await parser.getEntry(2);
261    }
262
263    const entries = new Map<TraceType, HierarchyTreeNode>();
264    entries.set(
265      TraceType.INPUT_METHOD_CLIENTS,
266      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb'),
267    );
268    entries.set(
269      TraceType.INPUT_METHOD_MANAGER_SERVICE,
270      await UnitTestUtils.getTraceEntry(
271        'traces/ime/InputMethodManagerService.pb',
272      ),
273    );
274    entries.set(
275      TraceType.INPUT_METHOD_SERVICE,
276      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodService.pb'),
277    );
278    entries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry);
279    entries.set(TraceType.WINDOW_MANAGER, windowManagerEntry);
280
281    const secondEntries = new Map<TraceType, HierarchyTreeNode>();
282    secondEntries.set(
283      TraceType.INPUT_METHOD_CLIENTS,
284      await UnitTestUtils.getTraceEntry('traces/ime/InputMethodClients.pb', 1),
285    );
286    secondEntries.set(TraceType.SURFACE_FLINGER, surfaceFlingerEntry);
287    secondEntries.set(TraceType.WINDOW_MANAGER, windowManagerEntry);
288
289    return [entries, secondEntries];
290  }
291
292  static timestampEqualityTester(first: any, second: any): boolean | undefined {
293    if (first instanceof Timestamp && second instanceof Timestamp) {
294      return UnitTestUtils.testTimestamps(first, second);
295    }
296    return undefined;
297  }
298
299  static checkSectionCollapseAndExpand<T>(
300    htmlElement: HTMLElement,
301    fixture: ComponentFixture<T>,
302    selector: string,
303    sectionTitle: string,
304  ) {
305    const section = assertDefined(htmlElement.querySelector(selector));
306    const collapseButton = assertDefined(
307      section.querySelector('collapsible-section-title button'),
308    ) as HTMLElement;
309    collapseButton.click();
310    fixture.detectChanges();
311    expect(section.classList).toContain('collapsed');
312    const collapsedSections = assertDefined(
313      htmlElement.querySelector('collapsed-sections'),
314    );
315    const collapsedSection = assertDefined(
316      collapsedSections.querySelector('.collapsed-section'),
317    ) as HTMLElement;
318    expect(collapsedSection.textContent).toContain(sectionTitle);
319    collapsedSection.click();
320    fixture.detectChanges();
321    UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement);
322  }
323
324  static checkNoCollapsedSectionButtons(htmlElement: HTMLElement) {
325    const collapsedSections = assertDefined(
326      htmlElement.querySelector('collapsed-sections'),
327    );
328    expect(
329      collapsedSections.querySelectorAll('.collapsed-section').length,
330    ).toEqual(0);
331  }
332
333  private static testTimestamps(
334    timestamp: Timestamp,
335    expectedTimestamp: Timestamp,
336  ): boolean {
337    if (timestamp.format() !== expectedTimestamp.format()) return false;
338    if (timestamp.getValueNs() !== expectedTimestamp.getValueNs()) {
339      return false;
340    }
341    return true;
342  }
343
344  private static async getTraceEntry<T>(filename: string, index = 0) {
345    const parser = (await UnitTestUtils.getParser(filename)) as Parser<T>;
346    return parser.getEntry(index);
347  }
348}
349
350export {UnitTestUtils};
351