chrome-devtools-frontend
Version:
Chrome DevTools UI
392 lines (326 loc) • 12.5 kB
text/typescript
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Lantern from '../lantern.js';
const {BaseNode, NetworkNode} = Lantern.Graph;
function sortedById(nodeArray: Lantern.Graph.Node[]) {
return nodeArray.sort((node1, node2) => node1.id.localeCompare(node2.id));
}
/**
* In the real implementation we pass around Node<T>, which are either CPUNodes
* or NetworkNodes. Rather than construct those for these tests, which don't
* need real nodes, we instead create BaseNodes and cast them to satisfy TS.
*/
function makeFakeNode(name: string): Lantern.Graph.Node {
return new BaseNode(name) as Lantern.Graph.Node;
}
function createComplexGraph() {
// B F
// / \ /
// A D - E
// \ / \
// C G - H
const nodeA = makeFakeNode('A');
const nodeB = makeFakeNode('B');
const nodeC = makeFakeNode('C');
const nodeD = makeFakeNode('D');
const nodeE = makeFakeNode('E');
const nodeF = makeFakeNode('F');
const nodeG = makeFakeNode('G');
const nodeH = makeFakeNode('H');
nodeA.addDependent(nodeB);
nodeA.addDependent(nodeC);
nodeB.addDependent(nodeD);
nodeC.addDependent(nodeD);
nodeD.addDependent(nodeE);
nodeE.addDependent(nodeF);
nodeE.addDependent(nodeG);
nodeG.addDependent(nodeH);
return {
nodeA,
nodeB,
nodeC,
nodeD,
nodeE,
nodeF,
nodeG,
nodeH,
};
}
describe('BaseNode', () => {
describe('#constructor', () => {
it('should set the ID', () => {
const node = makeFakeNode('foo');
assert.strictEqual(node.id, 'foo');
});
});
describe('.addDependent', () => {
it('should add the correct edge', () => {
const nodeA = makeFakeNode('1');
const nodeB = makeFakeNode('2');
nodeA.addDependent(nodeB);
assert.deepEqual(nodeA.getDependents(), [nodeB]);
assert.deepEqual(nodeB.getDependencies(), [nodeA]);
});
});
describe('.addDependency', () => {
it('should add the correct edge', () => {
const nodeA = makeFakeNode('1');
const nodeB = makeFakeNode('2');
nodeA.addDependency(nodeB);
assert.deepEqual(nodeA.getDependencies(), [nodeB]);
assert.deepEqual(nodeB.getDependents(), [nodeA]);
});
it('throw when trying to add a dependency on itself', () => {
const nodeA = makeFakeNode('1');
expect(() => nodeA.addDependency(nodeA)).to.throw();
});
});
describe('.isDependentOn', () => {
it('should identify the dependency relationships', () => {
const graph = createComplexGraph();
const nodes = Object.values(graph);
const {nodeA, nodeB, nodeD, nodeF, nodeH} = graph;
for (const node of nodes) {
expect(nodeA.isDependentOn(node)).equals(node === nodeA);
expect(nodeB.isDependentOn(node)).equals(node === nodeA || node === nodeB);
expect(nodeH.isDependentOn(node)).equals(node !== nodeF);
}
expect(nodeD.isDependentOn(nodeA)).equals(true);
expect(nodeD.isDependentOn(nodeB)).equals(true);
expect(nodeD.isDependentOn(nodeD)).equals(true);
expect(nodeD.isDependentOn(nodeH)).equals(false);
expect(nodeH.isDependentOn(nodeD)).equals(true);
expect(nodeF.isDependentOn(nodeH)).equals(false);
expect(nodeH.isDependentOn(nodeF)).equals(false);
});
});
describe('.getRootNode', () => {
it('should return the root node', () => {
const graph = createComplexGraph();
assert.strictEqual(graph.nodeA.getRootNode(), graph.nodeA);
assert.strictEqual(graph.nodeB.getRootNode(), graph.nodeA);
assert.strictEqual(graph.nodeD.getRootNode(), graph.nodeA);
assert.strictEqual(graph.nodeF.getRootNode(), graph.nodeA);
});
});
describe('.cloneWithoutRelationships', () => {
it('should create a copy', () => {
const node = makeFakeNode('1');
const neighbor = makeFakeNode('2');
node.addDependency(neighbor);
const clone = node.cloneWithoutRelationships();
assert.strictEqual(clone.id, '1');
assert.notEqual(node, clone);
assert.lengthOf(clone.getDependencies(), 0);
});
it('should copy isMainDocument', () => {
const node = makeFakeNode('1');
node.setIsMainDocument(true);
const networkNode = new NetworkNode({} as Lantern.Types.NetworkRequest);
networkNode.setIsMainDocument(true);
assert.isOk(node.cloneWithoutRelationships().isMainDocument());
assert.isOk(networkNode.cloneWithoutRelationships().isMainDocument());
});
});
describe('.cloneWithRelationships', () => {
it('should create a copy of a basic graph', () => {
const node = makeFakeNode('1');
const neighbor = makeFakeNode('2');
node.addDependency(neighbor);
const clone = node.cloneWithRelationships();
assert.strictEqual(clone.id, '1');
assert.notEqual(node, clone);
const dependencies = clone.getDependencies();
assert.lengthOf(dependencies, 1);
const neighborClone = dependencies[0];
assert.strictEqual(neighborClone.id, neighbor.id);
assert.notEqual(neighborClone, neighbor);
assert.strictEqual(neighborClone.getDependents()[0], clone);
});
it('should create a copy of a complex graph', () => {
const graph = createComplexGraph();
const clone = graph.nodeA.cloneWithRelationships();
const clonedIdMap = new Map();
clone.traverse(node => clonedIdMap.set(node.id, node));
assert.strictEqual(clonedIdMap.size, 8);
graph.nodeA.traverse(node => {
const clone = clonedIdMap.get(node.id);
assert.strictEqual(clone.id, node.id);
assert.notEqual(clone, node);
const actualDependents = sortedById(clone.getDependents());
const expectedDependents = sortedById(node.getDependents());
actualDependents.forEach((cloneDependent, index) => {
const originalDependent = expectedDependents[index];
assert.strictEqual(cloneDependent.id, originalDependent.id);
assert.notEqual(cloneDependent, originalDependent);
});
});
});
it('should create a copy of a graph with long dependency chains', () => {
// C - D - E - F
// / \
// A - - - - - - - B
const nodeA = makeFakeNode('A');
const nodeB = makeFakeNode('B');
const nodeC = makeFakeNode('C');
const nodeD = makeFakeNode('D');
const nodeE = makeFakeNode('E');
const nodeF = makeFakeNode('F');
nodeA.addDependent(nodeB);
nodeF.addDependent(nodeB);
nodeA.addDependent(nodeC);
nodeC.addDependent(nodeD);
nodeD.addDependent(nodeE);
nodeE.addDependent(nodeF);
const clone = nodeA.cloneWithRelationships();
const clonedIdMap = new Map();
clone.traverse(node => clonedIdMap.set(node.id, node));
assert.strictEqual(clonedIdMap.size, 6);
});
it('should create a copy when not starting at root node', () => {
const graph = createComplexGraph();
const cloneD = graph.nodeD.cloneWithRelationships();
assert.strictEqual(cloneD.id, 'D');
assert.strictEqual(cloneD.getRootNode().id, 'A');
});
it('should create a partial copy of a complex graph', () => {
const graph = createComplexGraph();
// create a clone with F and all its dependencies
const clone = graph.nodeA.cloneWithRelationships(node => node.id === 'F');
const clonedIdMap = new Map();
clone.traverse(node => clonedIdMap.set(node.id, node));
assert.strictEqual(clonedIdMap.size, 6);
assert.isOk(clonedIdMap.has('F'), 'did not include target node');
assert.isOk(clonedIdMap.has('E'), 'did not include dependency');
assert.isOk(clonedIdMap.has('B'), 'did not include branched dependency');
assert.isOk(clonedIdMap.has('C'), 'did not include branched dependency');
assert.isUndefined(clonedIdMap.get('G'));
assert.isUndefined(clonedIdMap.get('H'));
});
it('should throw if original node is not in cloned graph', () => {
const graph = createComplexGraph();
assert.throws(
// clone from root to nodeB, but called on nodeD
() => graph.nodeD.cloneWithRelationships(node => node.id === 'B'),
/^Cloned graph missing node$/,
);
});
});
describe('.traverse', () => {
it('should visit every dependent node', () => {
const graph = createComplexGraph();
const ids: string[] = [];
graph.nodeA.traverse(node => ids.push(node.id));
assert.deepEqual(ids, ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);
});
it('should include a shortest traversal path to every dependent node', () => {
const graph = createComplexGraph();
const paths: string[][] = [];
graph.nodeA.traverse((node, traversalPath) => {
assert.strictEqual(node.id, traversalPath[0].id);
paths.push(traversalPath.map(node => node.id));
});
assert.deepEqual(paths, [
['A'],
['B', 'A'],
['C', 'A'],
['D', 'B', 'A'],
['E', 'D', 'B', 'A'],
['F', 'E', 'D', 'B', 'A'],
['G', 'E', 'D', 'B', 'A'],
['H', 'G', 'E', 'D', 'B', 'A'],
]);
});
it('should respect getNext', () => {
const graph = createComplexGraph();
const ids: string[] = [];
graph.nodeF.traverse(
node => ids.push(node.id),
node => node.getDependencies(),
);
assert.deepEqual(ids, ['F', 'E', 'D', 'B', 'C', 'A']);
});
});
describe('#hasCycle', () => {
it('should return false for DAGs', () => {
const graph = createComplexGraph();
assert.isNull(BaseNode.findCycle(graph.nodeA));
});
it('should return false for triangular DAGs', () => {
// B
// / \
// A - C
const nodeA = makeFakeNode('A');
const nodeB = makeFakeNode('B');
const nodeC = makeFakeNode('C');
nodeA.addDependent(nodeC);
nodeA.addDependent(nodeB);
nodeB.addDependent(nodeC);
assert.isNull(BaseNode.findCycle(nodeA));
});
it('should return true for basic cycles', () => {
// A - B - C - A!
const nodeA = makeFakeNode('A');
const nodeB = makeFakeNode('B');
const nodeC = makeFakeNode('C');
nodeA.addDependent(nodeB);
nodeB.addDependent(nodeC);
nodeC.addDependent(nodeA);
assert.isNotNull(BaseNode.findCycle(nodeA));
});
it('should return true for children', () => {
// A!
// /
// A - B - C
const nodeA = makeFakeNode('A');
const nodeB = makeFakeNode('B');
const nodeC = makeFakeNode('C');
nodeA.addDependent(nodeB);
nodeB.addDependent(nodeC);
nodeB.addDependent(nodeA);
assert.isNotNull(BaseNode.findCycle(nodeC));
});
it('should return true for complex cycles', () => {
// B - D - F - G - C!
// / /
// A - - C - E - H
const nodeA = makeFakeNode('A');
const nodeB = makeFakeNode('B');
const nodeC = makeFakeNode('C');
const nodeD = makeFakeNode('D');
const nodeE = makeFakeNode('E');
const nodeF = makeFakeNode('F');
const nodeG = makeFakeNode('G');
const nodeH = makeFakeNode('H');
nodeA.addDependent(nodeB);
nodeA.addDependent(nodeC);
nodeB.addDependent(nodeD);
nodeC.addDependent(nodeE);
nodeC.addDependent(nodeF);
nodeD.addDependent(nodeF);
nodeE.addDependent(nodeH);
nodeF.addDependent(nodeG);
nodeG.addDependent(nodeC);
assert.isNotNull(BaseNode.findCycle(nodeA));
assert.isNotNull(BaseNode.findCycle(nodeB));
assert.isNotNull(BaseNode.findCycle(nodeC));
assert.isNotNull(BaseNode.findCycle(nodeD));
assert.isNotNull(BaseNode.findCycle(nodeE));
assert.isNotNull(BaseNode.findCycle(nodeF));
assert.isNotNull(BaseNode.findCycle(nodeG));
assert.isNotNull(BaseNode.findCycle(nodeH));
});
it('works for very large graphs', () => {
const root = makeFakeNode('root');
let lastNode = root;
for (let i = 0; i < 10000; i++) {
const nextNode = makeFakeNode(`child${i}`);
lastNode.addDependent(nextNode);
lastNode = nextNode;
}
lastNode.addDependent(root);
assert.isNotNull(BaseNode.findCycle(root));
});
});
});