blob: bfede262a6ea6cae93e7b36f63e9b025be5aafa9 [file]
// Copyright (C) 2024 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 {AggregationNode} from './query_builder/nodes/aggregation_node';
import {ModifyColumnsNode} from './query_builder/nodes/modify_columns_node';
import {ExplorePageState} from './explore_page';
import {IntervalIntersectNode} from './query_builder/nodes/interval_intersect_node';
import {SlicesSourceNode} from './query_builder/nodes/sources/slices_source';
import {SqlSourceNode} from './query_builder/nodes/sources/sql_source';
import {TableSourceNode} from './query_builder/nodes/sources/table_source';
import {serializeState, deserializeState} from './json_handler';
import {Trace} from '../../public/trace';
import {
SqlModules,
SqlTable,
SqlType,
} from '../../plugins/dev.perfetto.SqlModules/sql_modules';
import {AddColumnsNode} from './query_builder/nodes/dev/add_columns_node';
import {LimitAndOffsetNode} from './query_builder/nodes/dev/limit_and_offset_node';
import {SortNode} from './query_builder/nodes/dev/sort_node';
describe('JSON serialization/deserialization', () => {
let trace: Trace;
let sqlModules: SqlModules;
beforeEach(() => {
trace = {
traceInfo: {
traceTitle: 'test_trace',
},
} as Trace;
const stringType: SqlType = {name: 'STRING', shortName: 'string'};
const timestampType: SqlType = {
name: 'TIMESTAMP_NS',
shortName: 'timestamp_ns',
};
const tables = new Map<string, SqlTable>();
tables.set('slice', {
name: 'slice',
description: '',
type: 'table',
includeKey: '',
idColumn: undefined,
linkedIdColumns: [],
joinIdColumns: [],
getTableColumns: () => [],
getIdColumns: () => [],
getJoinIdColumns: () => [],
getIdTables: () => [],
getJoinIdTables: () => [],
columns: [
{
name: 'name',
type: stringType,
},
{
name: 'ts',
type: timestampType,
},
{
name: 'dur',
type: timestampType,
},
],
});
sqlModules = {
getTable: (name: string) => tables.get(name),
} as SqlModules;
});
test('serializes and deserializes a simple graph', () => {
const sliceNode = new SlicesSourceNode({
slice_name: 'test_slice',
});
const initialState: ExplorePageState = {
rootNodes: [sliceNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedNode = deserializedState.rootNodes[0];
expect((deserializedNode as SlicesSourceNode).state.slice_name).toBe(
'test_slice',
);
});
test('handles multiple nodes and connections', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const modifyNode = new ModifyColumnsNode({
prevNode: tableNode,
newColumns: [],
selectedColumns: [],
});
tableNode.nextNodes.push(modifyNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map([
[tableNode.nodeId, {x: 10, y: 20}],
[modifyNode.nodeId, {x: 100, y: 200}],
]),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedModifyNode = deserializedTableNode.nextNodes[0];
expect((deserializedModifyNode as ModifyColumnsNode).prevNode?.nodeId).toBe(
deserializedTableNode.nodeId,
);
expect(
deserializedState.nodeLayouts.get(deserializedTableNode.nodeId),
).toEqual({x: 10, y: 20});
});
test('serializes and deserializes aggregation node', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const sliceTable = sqlModules.getTable('slice')!;
const aggregationNode = new AggregationNode({
prevNode: tableNode,
groupByColumns: [
{
name: 'name',
type: 'STRING',
checked: true,
column: sliceTable.columns[0],
},
],
aggregations: [
{
column: {
name: 'dur',
type: 'TIMESTAMP_NS',
checked: true,
column: sliceTable.columns[2],
},
aggregationOp: 'SUM',
newColumnName: 'total_dur',
},
],
});
tableNode.nextNodes.push(aggregationNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedAggregationNode = deserializedTableNode
.nextNodes[0] as AggregationNode;
expect(deserializedAggregationNode.prevNode?.nodeId).toBe(
deserializedTableNode.nodeId,
);
expect(deserializedAggregationNode.state.groupByColumns[0].name).toBe(
'name',
);
expect(
deserializedAggregationNode.state.aggregations[0].aggregationOp,
).toBe('SUM');
expect(deserializedAggregationNode.state.aggregations[0].column?.name).toBe(
'dur',
);
});
test('serializes and deserializes sql source node', () => {
const sqlNode = new SqlSourceNode({
sql: 'SELECT * FROM slice',
trace,
});
const initialState: ExplorePageState = {
rootNodes: [sqlNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedNode = deserializedState.rootNodes[0] as SqlSourceNode;
expect(deserializedNode.state.sql).toBe('SELECT * FROM slice');
expect(deserializedNode.prevNodes).toEqual([]);
});
test('serializes and deserializes interval intersect node', () => {
const tableNode1 = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const tableNode2 = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const intervalIntersectNode = new IntervalIntersectNode({
prevNodes: [tableNode1, tableNode2],
allNodes: [tableNode1, tableNode2],
});
tableNode1.nextNodes.push(intervalIntersectNode);
tableNode2.nextNodes.push(intervalIntersectNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode1, tableNode2],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(2);
const deserializedTableNode1 = deserializedState.rootNodes[0];
const deserializedTableNode2 = deserializedState.rootNodes[1];
expect(deserializedTableNode1.nextNodes.length).toBe(1);
const deserializedIntervalIntersectNode = deserializedTableNode1
.nextNodes[0] as IntervalIntersectNode;
expect(deserializedIntervalIntersectNode.prevNodes).toBeDefined();
expect(deserializedIntervalIntersectNode.prevNodes?.length).toBe(2);
expect(deserializedIntervalIntersectNode.prevNodes?.[0].nodeId).toBe(
deserializedTableNode1.nodeId,
);
expect(deserializedIntervalIntersectNode.prevNodes?.[1].nodeId).toBe(
deserializedTableNode2.nodeId,
);
});
test('serializes and deserializes sql source node with reference', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const modifyNode = new ModifyColumnsNode({
prevNode: tableNode,
newColumns: [],
selectedColumns: [],
});
tableNode.nextNodes.push(modifyNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedModifyNode = deserializedTableNode
.nextNodes[0] as ModifyColumnsNode;
expect(deserializedModifyNode.prevNode?.nodeId).toBe(
deserializedTableNode.nodeId,
);
});
test('serializes and deserializes node with filters', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
filters: [
{
column: 'name',
op: '=',
value: 'test',
},
],
});
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState
.rootNodes[0] as TableSourceNode;
expect(deserializedTableNode.state.filters?.length).toBe(1);
const filter = deserializedTableNode.state.filters?.[0];
expect(filter?.column).toBe('name');
expect(filter?.op).toBe('=');
if (filter !== undefined && 'value' in filter) {
expect(filter.value).toBe('test');
} else {
fail('Filter value not found');
}
});
test('serializes and deserializes node layouts', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const sliceNode = new SlicesSourceNode({
slice_name: 'test_slice',
});
const initialState: ExplorePageState = {
rootNodes: [tableNode, sliceNode],
nodeLayouts: new Map([
[tableNode.nodeId, {x: 10, y: 20}],
[sliceNode.nodeId, {x: 100, y: 200}],
]),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(2);
const deserializedTableNode = deserializedState.rootNodes[0];
const deserializedSliceNode = deserializedState.rootNodes[1];
expect(
deserializedState.nodeLayouts.get(deserializedTableNode.nodeId),
).toEqual({x: 10, y: 20});
expect(
deserializedState.nodeLayouts.get(deserializedSliceNode.nodeId),
).toEqual({x: 100, y: 200});
});
test('serializes and deserializes node with BigInt filter', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
filters: [
{
column: 'id',
op: '=',
value: 12345678901234567890n,
},
],
});
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState
.rootNodes[0] as TableSourceNode;
expect(deserializedTableNode.state.filters?.length).toBe(1);
const filter = deserializedTableNode.state.filters?.[0];
expect(filter?.column).toBe('id');
expect(filter?.op).toBe('=');
if (filter !== undefined && 'value' in filter) {
expect(filter.value).toBe('12345678901234567890');
} else {
fail('Filter value not found');
}
});
test('serializes and deserializes an empty graph', () => {
const initialState: ExplorePageState = {
rootNodes: [],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(0);
expect(deserializedState.nodeLayouts.size).toBe(0);
});
test('throws error on invalid json', () => {
const invalidJson = '{"invalid": "json"}';
expect(() => deserializeState(invalidJson, trace, sqlModules)).toThrow();
});
test('deserializes graph with and without prevNodes', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const modifyNode = new ModifyColumnsNode({
prevNode: tableNode,
newColumns: [],
selectedColumns: [],
});
tableNode.nextNodes.push(modifyNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
// Test with prevNode
const jsonWithPrevNode = serializeState(initialState);
const deserializedStateWithPrevNode = deserializeState(
jsonWithPrevNode,
trace,
sqlModules,
);
const deserializedTableNode1 = deserializedStateWithPrevNode.rootNodes[0];
const deserializedModifyNode1 = deserializedTableNode1
.nextNodes[0] as ModifyColumnsNode;
expect(deserializedModifyNode1.prevNode?.nodeId).toBe(
deserializedTableNode1.nodeId,
);
});
test('serializes and deserializes custom title', () => {
const sliceNode = new SlicesSourceNode({
slice_name: 'test_slice',
customTitle: 'My Custom Title',
});
const initialState: ExplorePageState = {
rootNodes: [sliceNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedNode = deserializedState.rootNodes[0] as SlicesSourceNode;
expect(deserializedNode.state.customTitle).toBe('My Custom Title');
expect(deserializedNode.getTitle()).toBe('My Custom Title');
});
test('serializes and deserializes modify columns node', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const modifyColumnsNode = new ModifyColumnsNode({
prevNode: tableNode,
newColumns: [{expression: '1', name: 'new_col'}],
selectedColumns: [],
filters: [
{
column: 'new_col',
op: '=',
value: '1',
},
],
});
tableNode.nextNodes.push(modifyColumnsNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedNode = deserializedTableNode
.nextNodes[0] as ModifyColumnsNode;
expect(deserializedNode.state.newColumns.length).toBe(1);
expect(deserializedNode.state.newColumns[0].name).toBe('new_col');
const filters = deserializedNode.state.filters;
expect(filters).toBeDefined();
if (filters) {
expect(filters.length).toBe(1);
expect(filters[0].column).toBe('new_col');
}
});
test('serializes and deserializes add columns node', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const addColumnsNode = new AddColumnsNode({
prevNode: tableNode,
sqlTable: sqlModules.getTable('slice')!,
selectedColumns: ['name'],
});
tableNode.nextNodes.push(addColumnsNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedNode = deserializedTableNode
.nextNodes[0] as AddColumnsNode;
expect(deserializedNode.state.selectedColumns).toEqual(['name']);
expect(deserializedNode.state.sqlTable?.name).toEqual('slice');
});
test('serializes and deserializes limit and offset node', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const limitAndOffsetNode = new LimitAndOffsetNode({
prevNode: tableNode,
limit: 100,
offset: 20,
});
tableNode.nextNodes.push(limitAndOffsetNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedNode = deserializedTableNode
.nextNodes[0] as LimitAndOffsetNode;
expect(deserializedNode.state.limit).toEqual(100);
expect(deserializedNode.state.offset).toEqual(20);
});
test('serializes and deserializes sort node', () => {
const tableNode = new TableSourceNode({
sqlTable: sqlModules.getTable('slice'),
trace,
sqlModules,
});
const sortNode = new SortNode({
prevNode: tableNode,
sortColNames: ['name', 'ts'],
});
tableNode.nextNodes.push(sortNode);
const initialState: ExplorePageState = {
rootNodes: [tableNode],
nodeLayouts: new Map(),
};
const json = serializeState(initialState);
const deserializedState = deserializeState(json, trace, sqlModules);
expect(deserializedState.rootNodes.length).toBe(1);
const deserializedTableNode = deserializedState.rootNodes[0];
expect(deserializedTableNode.nextNodes.length).toBe(1);
const deserializedNode = deserializedTableNode.nextNodes[0] as SortNode;
expect(deserializedNode.state.sortColNames).toEqual(['name', 'ts']);
});
});