xref: /aosp_15_r20/development/tools/winscope/src/viewers/common/log_presenter_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2024 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 {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
18import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
19import {TraceBuilder} from 'test/unit/trace_builder';
20import {TraceType} from 'trace/trace_type';
21import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
22import {TextFilter} from 'viewers/common/text_filter';
23import {LogSelectFilter, LogTextFilter} from './log_filters';
24import {LogPresenter} from './log_presenter';
25import {LogEntry, LogHeader} from './ui_data_log';
26
27describe('LogPresenter', () => {
28  let presenter: LogPresenter<LogEntry>;
29  const timestamp1 = TimestampConverterUtils.makeElapsedTimestamp(1n);
30  const timestamp2 = TimestampConverterUtils.makeElapsedTimestamp(2n);
31  const timestamp3 = TimestampConverterUtils.makeElapsedTimestamp(3n);
32  const timestamp4 = TimestampConverterUtils.makeElapsedTimestamp(4n);
33  const trace = new TraceBuilder<PropertyTreeNode>()
34    .setType(TraceType.TRANSACTIONS)
35    .setEntries([
36      new PropertyTreeBuilder()
37        .setRootId('Test Trace')
38        .setName('entry 1')
39        .build(),
40      new PropertyTreeBuilder()
41        .setRootId('Test Trace')
42        .setName('entry 2')
43        .build(),
44      new PropertyTreeBuilder()
45        .setRootId('Test Trace')
46        .setName('entry 3')
47        .build(),
48      new PropertyTreeBuilder()
49        .setRootId('Test Trace')
50        .setName('entry 4')
51        .build(),
52    ])
53    .setTimestamps([timestamp1, timestamp2, timestamp3, timestamp4])
54    .build();
55
56  const STRING_COLUMN = {
57    name: 'String Column',
58    cssClass: 'string-column',
59  };
60  const NUMBER_COLUMN = {
61    name: 'Number Column',
62    cssClass: 'number-column',
63  };
64  const TIMESTAMP_COLUMN = {
65    name: 'Timestamp Column',
66    cssClass: 'timestamp-column',
67  };
68  let stringFilter: LogTextFilter;
69  let numberFilter: LogSelectFilter;
70  let headers: LogHeader[];
71  let testEntries: LogEntry[];
72
73  describe('time-ordered entries', () => {
74    beforeEach(async () => {
75      presenter = new LogPresenter();
76      testEntries = await buildTestEntries();
77      presenter.setAllEntries(testEntries);
78      expectAllIndicesUndefined();
79
80      stringFilter = new LogTextFilter(new TextFilter('stringValue'));
81      numberFilter = new LogSelectFilter(['0', '1', '2', '3']);
82      headers = [
83        new LogHeader(STRING_COLUMN, stringFilter),
84        new LogHeader(NUMBER_COLUMN, numberFilter),
85        new LogHeader(TIMESTAMP_COLUMN),
86      ];
87    });
88
89    it('applies filters to existing entries when headers updated', async () => {
90      presenter.setHeaders(headers);
91      expect(presenter.getHeaders()).toEqual(headers);
92      expect(presenter.getFilteredEntries()).toEqual([
93        testEntries[0],
94        testEntries[2],
95      ]);
96      expectAllIndicesUndefined();
97    });
98
99    it('applies existing filters when entries updated', async () => {
100      presenter.setAllEntries([]);
101      expectAllIndicesUndefined();
102
103      presenter.setHeaders(headers);
104      expect(presenter.getHeaders()).toEqual(headers);
105      expectAllIndicesUndefined();
106
107      presenter.setAllEntries(testEntries);
108      expect(presenter.getFilteredEntries()).toEqual([
109        testEntries[0],
110        testEntries[2],
111      ]);
112      expectAllIndicesUndefined();
113    });
114
115    it('applies log entry click', () => {
116      // selects index
117      presenter.applyLogEntryClick(1);
118      expect(presenter.getSelectedIndex()).toEqual(1);
119      expect(presenter.getCurrentIndex()).toBeUndefined();
120      expect(presenter.getScrollToIndex()).toBeUndefined();
121
122      // on same index, clears scroll but leaves current and selected unchanged
123      presenter.applyTracePositionUpdate(trace.getEntry(0));
124      presenter.applyLogEntryClick(1);
125      expect(presenter.getSelectedIndex()).toEqual(1);
126      expect(presenter.getCurrentIndex()).toEqual(0);
127      expect(presenter.getScrollToIndex()).toBeUndefined();
128    });
129
130    it('applies arrow down press', () => {
131      // selects and scrolls to first index if no selected or current index
132      presenter.applyArrowDownPress();
133      expect(presenter.getSelectedIndex()).toEqual(0);
134      expect(presenter.getScrollToIndex()).toEqual(0);
135      expect(presenter.getCurrentIndex()).toBeUndefined();
136
137      // selects next index after selected index
138      presenter.applyArrowDownPress();
139      expect(presenter.getSelectedIndex()).toEqual(1);
140      expect(presenter.getScrollToIndex()).toEqual(1);
141      expect(presenter.getCurrentIndex()).toBeUndefined();
142
143      // handles index out of range
144      presenter.applyArrowDownPress();
145      presenter.applyArrowDownPress();
146      presenter.applyArrowDownPress();
147      expect(presenter.getSelectedIndex()).toEqual(3);
148      expect(presenter.getScrollToIndex()).toEqual(3);
149      expect(presenter.getCurrentIndex()).toBeUndefined();
150
151      // selects next index after current index
152      presenter.applyTracePositionUpdate(trace.getEntry(0));
153      presenter.applyArrowDownPress();
154      expect(presenter.getSelectedIndex()).toEqual(1);
155      expect(presenter.getScrollToIndex()).toEqual(1);
156      expect(presenter.getCurrentIndex()).toEqual(0);
157
158      // handles no entries
159      presenter.setAllEntries([]);
160      presenter.applyArrowDownPress();
161      expectAllIndicesUndefined();
162    });
163
164    it('applies arrow up press', () => {
165      // selects first index if no selected or current index
166      presenter.applyArrowUpPress();
167      expect(presenter.getSelectedIndex()).toEqual(0);
168      expect(presenter.getScrollToIndex()).toEqual(0);
169      expect(presenter.getCurrentIndex()).toBeUndefined();
170
171      // selects index before selected index
172      presenter.applyLogEntryClick(2);
173      presenter.applyArrowUpPress();
174      expect(presenter.getSelectedIndex()).toEqual(1);
175      expect(presenter.getScrollToIndex()).toEqual(1);
176      expect(presenter.getCurrentIndex()).toBeUndefined();
177
178      // handles index out of range
179      presenter.applyArrowUpPress();
180      presenter.applyArrowUpPress();
181      presenter.applyArrowUpPress();
182      expect(presenter.getSelectedIndex()).toEqual(0);
183      expect(presenter.getScrollToIndex()).toEqual(0);
184      expect(presenter.getCurrentIndex()).toBeUndefined();
185
186      // selects index before current index
187      presenter.applyTracePositionUpdate(trace.getEntry(1));
188      presenter.applyArrowUpPress();
189      expect(presenter.getSelectedIndex()).toEqual(0);
190      expect(presenter.getScrollToIndex()).toEqual(0);
191      expect(presenter.getCurrentIndex()).toEqual(1);
192
193      // handles no entries
194      presenter.setAllEntries([]);
195      presenter.applyArrowDownPress();
196      expectAllIndicesUndefined();
197    });
198
199    it('applies trace position update', () => {
200      // updates current index, clears selected index, scrolls to current index
201      presenter.applyTracePositionUpdate(trace.getEntry(1));
202      expect(presenter.getCurrentIndex()).toEqual(1);
203      expect(presenter.getSelectedIndex()).toBeUndefined();
204      expect(presenter.getScrollToIndex()).toEqual(1);
205
206      // if no current entry, current index undefined
207      presenter.applyTracePositionUpdate(undefined);
208      expect(presenter.getCurrentIndex()).toBeUndefined();
209      expect(presenter.getSelectedIndex()).toBeUndefined();
210      expect(presenter.getScrollToIndex()).toBeUndefined();
211
212      // if current entry filtered out, returns next entry by time
213      updateStringFilterAndCheckEntries('stringValue', [
214        testEntries[0],
215        testEntries[2],
216      ]);
217      presenter.applyTracePositionUpdate(trace.getEntry(1));
218      expect(presenter.getCurrentIndex()).toEqual(1);
219      expect(presenter.getSelectedIndex()).toBeUndefined();
220      expect(presenter.getScrollToIndex()).toEqual(1);
221
222      // handles no filtered entries
223      updateStringFilterAndCheckEntries('no matches', []);
224      presenter.applyTracePositionUpdate(trace.getEntry(1));
225      expectAllIndicesUndefined();
226      updateStringFilterAndCheckEntries('', testEntries);
227
228      // handles no entries
229      presenter.setAllEntries([]);
230      presenter.applyTracePositionUpdate(trace.getEntry(1));
231      presenter.applyArrowDownPress();
232      expectAllIndicesUndefined();
233    });
234
235    it('applies trace position update for non time-ordered entries', () => {
236      const presenter = new LogPresenter(false);
237      presenter.setAllEntries([testEntries[3]].concat(testEntries.slice(0, 3)));
238      expectAllIndicesUndefined();
239
240      presenter.applyTracePositionUpdate(trace.getEntry(1));
241      expect(presenter.getCurrentIndex()).toEqual(2);
242      expect(presenter.getSelectedIndex()).toBeUndefined();
243      expect(presenter.getScrollToIndex()).toEqual(2);
244
245      presenter.applyTracePositionUpdate(trace.getEntry(3));
246      expect(presenter.getCurrentIndex()).toEqual(0);
247      expect(presenter.getSelectedIndex()).toBeUndefined();
248      expect(presenter.getScrollToIndex()).toEqual(0);
249    });
250
251    it('applies text filter change', () => {
252      updateStringFilterAndCheckEntries('stringValue', [
253        testEntries[0],
254        testEntries[2],
255      ]);
256      updateStringFilterAndCheckEntries('no matches', []);
257      updateStringFilterAndCheckEntries('', testEntries);
258    });
259
260    it('applies select filter change', () => {
261      updateNumberFilterAndCheckEntries(['0'], [testEntries[0]]);
262      updateNumberFilterAndCheckEntries(
263        ['0', '3'],
264        [testEntries[0], testEntries[3]],
265      );
266      updateNumberFilterAndCheckEntries([], testEntries);
267    });
268  });
269
270  async function buildTestEntries(): Promise<LogEntry[]> {
271    return [
272      {
273        traceEntry: trace.getEntry(0),
274        fields: [
275          {
276            spec: STRING_COLUMN,
277            value: 'stringValue',
278          },
279          {
280            spec: NUMBER_COLUMN,
281            value: 0,
282          },
283          {
284            spec: TIMESTAMP_COLUMN,
285            value: timestamp1,
286          },
287        ],
288        propertiesTree: await trace.getEntry(0).getValue(),
289      },
290      {
291        traceEntry: trace.getEntry(1),
292        fields: [
293          {
294            spec: STRING_COLUMN,
295            value: 'differentValue',
296          },
297          {
298            spec: NUMBER_COLUMN,
299            value: 1,
300          },
301          {
302            spec: TIMESTAMP_COLUMN,
303            value: timestamp2,
304          },
305        ],
306        propertiesTree: await trace.getEntry(1).getValue(),
307      },
308      {
309        traceEntry: trace.getEntry(2),
310        fields: [
311          {
312            spec: STRING_COLUMN,
313            value: 'stringValue',
314          },
315          {
316            spec: NUMBER_COLUMN,
317            value: 2,
318          },
319          {
320            spec: TIMESTAMP_COLUMN,
321            value: timestamp3,
322          },
323        ],
324        propertiesTree: await trace.getEntry(2).getValue(),
325      },
326      {
327        traceEntry: trace.getEntry(3),
328        fields: [
329          {
330            spec: STRING_COLUMN,
331            value: 'differentValue',
332          },
333          {
334            spec: NUMBER_COLUMN,
335            value: 3,
336          },
337          {
338            spec: TIMESTAMP_COLUMN,
339            value: timestamp4,
340          },
341        ],
342        propertiesTree: await trace.getEntry(3).getValue(),
343      },
344    ];
345  }
346
347  function expectAllIndicesUndefined() {
348    expect(presenter.getCurrentIndex()).toBeUndefined();
349    expect(presenter.getSelectedIndex()).toBeUndefined();
350    expect(presenter.getScrollToIndex()).toBeUndefined();
351  }
352
353  function updateStringFilterAndCheckEntries(
354    value: string,
355    expectedEntries: LogEntry[],
356  ) {
357    stringFilter.updateFilterValue([value]);
358    presenter.applyTextFilterChange(headers[0], stringFilter.textFilter);
359    expect(presenter.getFilteredEntries()).toEqual(expectedEntries);
360  }
361
362  function updateNumberFilterAndCheckEntries(
363    value: string[],
364    expectedEntries: LogEntry[],
365  ) {
366    presenter.applySelectFilterChange(headers[1], value);
367    expect(presenter.getFilteredEntries()).toEqual(expectedEntries);
368  }
369});
370