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 {assertDefined} from 'common/assert_utils';
18import {FileUtils} from 'common/file_utils';
19import {ProgressListenerStub} from 'messaging/progress_listener_stub';
20import {UserWarning} from 'messaging/user_warning';
21import {
22  CorruptedArchive,
23  InvalidPerfettoTrace,
24  NoInputFiles,
25  TraceOverridden,
26  UnsupportedFileFormat,
27} from 'messaging/user_warnings';
28import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
29import {TracesUtils} from 'test/unit/traces_utils';
30import {UnitTestUtils} from 'test/unit/utils';
31import {TraceType} from 'trace/trace_type';
32import {FilesSource} from './files_source';
33import {TracePipeline} from './trace_pipeline';
34
35describe('TracePipeline', () => {
36  let validSfFile: File;
37  let validWmFile: File;
38  let warnings: UserWarning[];
39  let progressListener: ProgressListenerStub;
40  let tracePipeline: TracePipeline;
41
42  beforeEach(async () => {
43    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
44    validSfFile = await UnitTestUtils.getFixtureFile(
45      'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
46    );
47    validWmFile = await UnitTestUtils.getFixtureFile(
48      'traces/elapsed_and_real_timestamp/WindowManager.pb',
49    );
50
51    warnings = [];
52
53    progressListener = new ProgressListenerStub();
54    spyOn(progressListener, 'onProgressUpdate');
55    spyOn(progressListener, 'onOperationFinished');
56
57    tracePipeline = new TracePipeline();
58  });
59
60  it('can load valid trace files', async () => {
61    expect(tracePipeline.getTraces().getSize()).toEqual(0);
62
63    await loadFiles([validSfFile, validWmFile], FilesSource.TEST);
64    await expectLoadResult(2, []);
65
66    expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
67      new RegExp(`${FilesSource.TEST}_`),
68    );
69    expect(tracePipeline.getTraces().getSize()).toEqual(2);
70
71    const traceEntries = await TracesUtils.extractEntries(
72      tracePipeline.getTraces(),
73    );
74    expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(
75      0,
76    );
77    expect(traceEntries.get(TraceType.SURFACE_FLINGER)?.length).toBeGreaterThan(
78      0,
79    );
80  });
81
82  it('can load valid gzipped file', async () => {
83    expect(tracePipeline.getTraces().getSize()).toEqual(0);
84
85    const gzippedFile = await UnitTestUtils.getFixtureFile(
86      'traces/WindowManager.pb.gz',
87    );
88
89    await loadFiles([gzippedFile], FilesSource.TEST);
90    await expectLoadResult(1, []);
91
92    expect(tracePipeline.getTraces().getSize()).toEqual(1);
93
94    const traceEntries = await TracesUtils.extractEntries(
95      tracePipeline.getTraces(),
96    );
97    expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(
98      0,
99    );
100  });
101
102  it('can set download archive filename based on files source', async () => {
103    await loadFiles([validSfFile]);
104    await expectLoadResult(1, []);
105    expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
106      new RegExp('SurfaceFlinger_'),
107    );
108
109    tracePipeline.clear();
110
111    await loadFiles([validSfFile, validWmFile], FilesSource.COLLECTED);
112    await expectLoadResult(2, []);
113    expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
114      new RegExp(`${FilesSource.COLLECTED}_`),
115    );
116  });
117
118  it('can convert illegal uploaded archive filename to legal name for download archive', async () => {
119    const fileWithIllegalName = await UnitTestUtils.getFixtureFile(
120      'traces/SFtrace(with_illegal_characters).pb',
121    );
122    await loadFiles([fileWithIllegalName]);
123    await expectLoadResult(1, []);
124    const downloadFilename = tracePipeline.getDownloadArchiveFilename();
125    expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test(downloadFilename)).toBeTrue();
126  });
127
128  it('detects bugreports and filters out files based on their directory', async () => {
129    expect(tracePipeline.getTraces().getSize()).toEqual(0);
130
131    const bugreportFiles = [
132      await UnitTestUtils.getFixtureFile(
133        'bugreports/main_entry.txt',
134        'main_entry.txt',
135      ),
136      await UnitTestUtils.getFixtureFile(
137        'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
138        'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
139      ),
140      await UnitTestUtils.getFixtureFile(
141        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
142        'FS/data/misc/wmtrace/surface_flinger.bp',
143      ),
144      await UnitTestUtils.getFixtureFile(
145        'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
146        'FS/data/misc/ignored-dir/window_manager.bp',
147      ),
148    ];
149
150    const bugreportArchive = new File(
151      [await FileUtils.createZipArchive(bugreportFiles)],
152      'bugreport.zip',
153    );
154
155    // Corner case:
156    // Another file is loaded along the bugreport -> the file must not be ignored
157    //
158    // Note:
159    // The even weirder corner case where two bugreports are loaded at the same time is
160    // currently not properly handled.
161    const otherFile = await UnitTestUtils.getFixtureFile(
162      'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
163      'would-be-ignored-if-was-in-bugreport-archive/input_method_clients.pb',
164    );
165
166    await loadFiles([bugreportArchive, otherFile]);
167    await expectLoadResult(2, []);
168
169    const traces = tracePipeline.getTraces();
170    expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined();
171    expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeUndefined(); // ignored
172    expect(traces.getTrace(TraceType.INPUT_METHOD_CLIENTS)).toBeDefined();
173  });
174
175  it('detects bugreports and extracts timezone info, then calculates utc offset', async () => {
176    const bugreportFiles = [
177      await UnitTestUtils.getFixtureFile(
178        'bugreports/main_entry.txt',
179        'main_entry.txt',
180      ),
181      await UnitTestUtils.getFixtureFile(
182        'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
183        'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
184      ),
185      await UnitTestUtils.getFixtureFile(
186        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
187        'FS/data/misc/wmtrace/surface_flinger.bp',
188      ),
189    ];
190    const bugreportArchive = new File(
191      [await FileUtils.createZipArchive(bugreportFiles)],
192      'bugreport.zip',
193    );
194
195    await loadFiles([bugreportArchive]);
196    await expectLoadResult(1, []);
197
198    const timestampConverter = tracePipeline.getTimestampConverter();
199    expect(timestampConverter);
200    expect(timestampConverter.getUTCOffset()).toEqual('UTC+05:30');
201
202    const expectedTimestamp =
203      TimestampConverterUtils.makeRealTimestampWithUTCOffset(
204        1659107089102062832n,
205      );
206    expect(
207      timestampConverter.makeTimestampFromMonotonicNs(14500282843n),
208    ).toEqual(expectedTimestamp);
209  });
210
211  it('is robust to corrupted archive', async () => {
212    const corruptedArchive = await UnitTestUtils.getFixtureFile(
213      'corrupted_archive.zip',
214    );
215
216    await loadFiles([corruptedArchive]);
217
218    await expectLoadResult(0, [
219      new CorruptedArchive(corruptedArchive),
220      new NoInputFiles(),
221    ]);
222  });
223
224  it('is robust to invalid trace files', async () => {
225    const invalidFiles = [
226      await UnitTestUtils.getFixtureFile('winscope_homepage.jpg'),
227    ];
228
229    await loadFiles(invalidFiles);
230
231    await expectLoadResult(0, [
232      new UnsupportedFileFormat('winscope_homepage.jpg'),
233    ]);
234  });
235
236  it('is robust to invalid perfetto trace files', async () => {
237    const invalidFiles = [
238      await UnitTestUtils.getFixtureFile(
239        'traces/perfetto/invalid_protolog.perfetto-trace',
240      ),
241    ];
242
243    await loadFiles(invalidFiles);
244
245    await expectLoadResult(0, [
246      new InvalidPerfettoTrace('invalid_protolog.perfetto-trace', [
247        'Perfetto trace has no IME Clients entries',
248        'Perfetto trace has no IME system_server entries',
249        'Perfetto trace has no IME Service entries',
250        'Perfetto trace has no ProtoLog entries',
251        'Perfetto trace has no Surface Flinger entries',
252        'Perfetto trace has no Transactions entries',
253        'Perfetto trace has no Transitions entries',
254        'Perfetto trace has no ViewCapture windows',
255        'Perfetto trace has no Motion Events entries',
256        'Perfetto trace has no Key Events entries',
257      ]),
258    ]);
259  });
260
261  it('is robust to mixed valid and invalid trace files', async () => {
262    expect(tracePipeline.getTraces().getSize()).toEqual(0);
263    const files = [
264      await UnitTestUtils.getFixtureFile('winscope_homepage.jpg'),
265      await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'),
266    ];
267
268    await loadFiles(files);
269
270    await expectLoadResult(1, [
271      new UnsupportedFileFormat('winscope_homepage.jpg'),
272    ]);
273  });
274
275  it('can remove traces', async () => {
276    await loadFiles([validSfFile, validWmFile]);
277    await expectLoadResult(2, []);
278
279    const sfTrace = assertDefined(
280      tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
281    );
282    const wmTrace = assertDefined(
283      tracePipeline.getTraces().getTrace(TraceType.WINDOW_MANAGER),
284    );
285
286    tracePipeline.removeTrace(sfTrace);
287    await expectLoadResult(1, []);
288
289    tracePipeline.removeTrace(wmTrace);
290    await expectLoadResult(0, []);
291  });
292
293  it('gets loaded traces', async () => {
294    await loadFiles([validSfFile, validWmFile]);
295    await expectLoadResult(2, []);
296
297    const traces = tracePipeline.getTraces();
298
299    const actualTraceTypes = new Set(traces.mapTrace((trace) => trace.type));
300    const expectedTraceTypes = new Set([
301      TraceType.SURFACE_FLINGER,
302      TraceType.WINDOW_MANAGER,
303    ]);
304    expect(actualTraceTypes).toEqual(expectedTraceTypes);
305
306    const sfTrace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER));
307    expect(sfTrace.getDescriptors().length).toBeGreaterThan(0);
308  });
309
310  it('gets screenrecording data', async () => {
311    const files = [
312      await UnitTestUtils.getFixtureFile(
313        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
314      ),
315    ];
316    await loadFiles(files);
317    await expectLoadResult(1, []);
318
319    const video = await tracePipeline.getScreenRecordingVideo();
320    expect(video).toBeDefined();
321    expect(video?.size).toBeGreaterThan(0);
322  });
323
324  it('gets screenshot data', async () => {
325    const files = [await UnitTestUtils.getFixtureFile('traces/screenshot.png')];
326    await loadFiles(files);
327    await expectLoadResult(1, []);
328
329    const video = await tracePipeline.getScreenRecordingVideo();
330    expect(video).toBeDefined();
331    expect(video?.size).toBeGreaterThan(0);
332  });
333
334  it('prioritises screenrecording over screenshot data', async () => {
335    const files = [
336      await UnitTestUtils.getFixtureFile('traces/screenshot.png'),
337      await UnitTestUtils.getFixtureFile(
338        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
339      ),
340    ];
341    await loadFiles(files);
342    await expectLoadResult(1, [
343      new TraceOverridden('screenshot.png', TraceType.SCREEN_RECORDING),
344    ]);
345
346    const video = await tracePipeline.getScreenRecordingVideo();
347    expect(video).toBeDefined();
348    expect(video?.size).toBeGreaterThan(0);
349  });
350
351  it('creates traces with correct type', async () => {
352    await loadFiles([validSfFile, validWmFile]);
353    await expectLoadResult(2, []);
354
355    const traces = tracePipeline.getTraces();
356    traces.forEachTrace((trace, type) => {
357      expect(trace.type).toEqual(type);
358    });
359  });
360
361  it('creates zip archive with loaded trace files', async () => {
362    const files = [
363      await UnitTestUtils.getFixtureFile(
364        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
365      ),
366      await UnitTestUtils.getFixtureFile(
367        'traces/perfetto/transactions_trace.perfetto-trace',
368      ),
369    ];
370    await loadFiles(files);
371    await expectLoadResult(2, []);
372
373    const archiveBlob =
374      await tracePipeline.makeZipArchiveWithLoadedTraceFiles();
375    const actualFiles = await FileUtils.unzipFile(archiveBlob);
376    const actualFilenames = actualFiles
377      .map((file) => {
378        return file.name;
379      })
380      .sort();
381
382    const expectedFilenames = [
383      'screen_recording_metadata_v2.mp4',
384      'transactions_trace.perfetto-trace',
385    ];
386
387    expect(actualFilenames).toEqual(expectedFilenames);
388  });
389
390  it('can be cleared', async () => {
391    await loadFiles([validSfFile, validWmFile]);
392    await expectLoadResult(2, []);
393
394    tracePipeline.clear();
395    expect(tracePipeline.getTraces().getSize()).toEqual(0);
396  });
397
398  it('can filter traces without visualization', async () => {
399    const shellTransitionFile = await UnitTestUtils.getFixtureFile(
400      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
401    );
402    await loadFiles([validSfFile, shellTransitionFile]);
403    await expectLoadResult(2, []);
404
405    tracePipeline.filterTracesWithoutVisualization();
406    expect(tracePipeline.getTraces().getSize()).toEqual(1);
407    expect(
408      tracePipeline.getTraces().getTrace(TraceType.SHELL_TRANSITION),
409    ).toBeUndefined();
410  });
411
412  async function loadFiles(
413    files: File[],
414    source: FilesSource = FilesSource.TEST,
415  ) {
416    const notificationListener = {
417      onNotifications(notifications: UserWarning[]) {
418        warnings.push(...notifications);
419      },
420    };
421    await tracePipeline.loadFiles(
422      files,
423      source,
424      notificationListener,
425      progressListener,
426    );
427    expect(progressListener.onOperationFinished).toHaveBeenCalled();
428    await tracePipeline.buildTraces();
429  }
430
431  async function expectLoadResult(
432    numberOfTraces: number,
433    expectedWarnings: UserWarning[],
434  ) {
435    expect(warnings).toEqual(expectedWarnings);
436    expect(tracePipeline.getTraces().getSize()).toEqual(numberOfTraces);
437  }
438});
439