344 lines
9.2 KiB
JavaScript
344 lines
9.2 KiB
JavaScript
// @flow strict-local
|
|
|
|
import assert from 'assert';
|
|
import sinon from 'sinon';
|
|
|
|
import Graph from '../src/Graph';
|
|
import {toNodeId} from '../src/types';
|
|
|
|
describe('Graph', () => {
|
|
it('constructor should initialize an empty graph', () => {
|
|
let graph = new Graph();
|
|
assert.deepEqual(graph.nodes, []);
|
|
assert.deepEqual([...graph.getAllEdges()], []);
|
|
});
|
|
|
|
it('addNode should add a node to the graph', () => {
|
|
let graph = new Graph();
|
|
let node = {};
|
|
let id = graph.addNode(node);
|
|
assert.equal(graph.getNode(id), node);
|
|
});
|
|
|
|
it('errors when traversing a graph with no root', () => {
|
|
let graph = new Graph();
|
|
|
|
assert.throws(() => {
|
|
graph.traverse(() => {});
|
|
}, /A start node is required to traverse/);
|
|
});
|
|
|
|
it("errors when traversing a graph with a startNode that doesn't belong", () => {
|
|
let graph = new Graph();
|
|
|
|
assert.throws(() => {
|
|
graph.traverse(() => {}, toNodeId(-1));
|
|
}, /Does not have node/);
|
|
});
|
|
|
|
it("errors if replaceNodeIdsConnectedTo is called with a node that doesn't belong", () => {
|
|
let graph = new Graph();
|
|
assert.throws(() => {
|
|
graph.replaceNodeIdsConnectedTo(toNodeId(-1), []);
|
|
}, /Does not have node/);
|
|
});
|
|
|
|
it("errors when adding an edge to a node that doesn't exist", () => {
|
|
let graph = new Graph();
|
|
let node = graph.addNode({});
|
|
assert.throws(() => {
|
|
graph.addEdge(node, toNodeId(-1));
|
|
}, /"to" node '-1' not found/);
|
|
});
|
|
|
|
it("errors when adding an edge from a node that doesn't exist", () => {
|
|
let graph = new Graph();
|
|
let node = graph.addNode({});
|
|
assert.throws(() => {
|
|
graph.addEdge(toNodeId(-1), node);
|
|
}, /"from" node '-1' not found/);
|
|
});
|
|
|
|
it('hasNode should return a boolean based on whether the node exists in the graph', () => {
|
|
let graph = new Graph();
|
|
let node = graph.addNode({});
|
|
assert(graph.hasNode(node));
|
|
assert(!graph.hasNode(toNodeId(-1)));
|
|
});
|
|
|
|
it('addEdge should add an edge to the graph', () => {
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
graph.addEdge(nodeA, nodeB);
|
|
assert(graph.hasEdge(nodeA, nodeB));
|
|
});
|
|
|
|
it('isOrphanedNode should return true or false if the node is orphaned or not', () => {
|
|
let graph = new Graph();
|
|
let rootNode = graph.addNode('root');
|
|
graph.setRootNodeId(rootNode);
|
|
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
graph.addEdge(rootNode, nodeB);
|
|
graph.addEdge(nodeB, nodeC, 1);
|
|
assert(graph.isOrphanedNode(nodeA));
|
|
assert(!graph.isOrphanedNode(nodeB));
|
|
assert(!graph.isOrphanedNode(nodeC));
|
|
});
|
|
|
|
it("removeEdge should throw if the edge doesn't exist", () => {
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
|
|
assert.throws(() => {
|
|
graph.removeEdge(nodeA, nodeB);
|
|
}, /Edge from 0 to 1 not found!/);
|
|
});
|
|
|
|
it('removeEdge should prune the graph at that edge', () => {
|
|
// a
|
|
// / \
|
|
// b - d
|
|
// /
|
|
// c
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
graph.setRootNodeId(nodeA);
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
let nodeD = graph.addNode('d');
|
|
graph.addEdge(nodeA, nodeB);
|
|
graph.addEdge(nodeA, nodeD);
|
|
graph.addEdge(nodeB, nodeC);
|
|
graph.addEdge(nodeB, nodeD);
|
|
|
|
graph.removeEdge(nodeA, nodeB);
|
|
assert(graph.hasNode(nodeA));
|
|
assert(graph.hasNode(nodeD));
|
|
assert(!graph.hasNode(nodeB));
|
|
assert(!graph.hasNode(nodeC));
|
|
assert.deepEqual(
|
|
[...graph.getAllEdges()],
|
|
[{from: nodeA, to: nodeD, type: 1}],
|
|
);
|
|
});
|
|
|
|
it('removing a node recursively deletes orphaned nodes', () => {
|
|
// before:
|
|
// a
|
|
// / \
|
|
// b c
|
|
// / \ \
|
|
// d e f
|
|
// /
|
|
// g
|
|
//
|
|
|
|
// after:
|
|
// a
|
|
// \
|
|
// c
|
|
// \
|
|
// f
|
|
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
graph.setRootNodeId(nodeA);
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
let nodeD = graph.addNode('d');
|
|
let nodeE = graph.addNode('e');
|
|
let nodeF = graph.addNode('f');
|
|
let nodeG = graph.addNode('g');
|
|
|
|
graph.addEdge(nodeA, nodeB);
|
|
graph.addEdge(nodeA, nodeC);
|
|
graph.addEdge(nodeB, nodeD);
|
|
graph.addEdge(nodeB, nodeE);
|
|
graph.addEdge(nodeC, nodeF);
|
|
graph.addEdge(nodeD, nodeG);
|
|
|
|
graph.removeNode(nodeB);
|
|
|
|
assert.deepEqual(graph.nodes.filter(Boolean), ['a', 'c', 'f']);
|
|
assert.deepEqual(Array.from(graph.getAllEdges()), [
|
|
{from: nodeA, to: nodeC, type: 1},
|
|
{from: nodeC, to: nodeF, type: 1},
|
|
]);
|
|
});
|
|
|
|
it('removing a node recursively deletes orphaned nodes if there is no path to the root', () => {
|
|
// before:
|
|
// a
|
|
// / \
|
|
// b c
|
|
// / \ \
|
|
// |-d e f
|
|
// |/
|
|
// g
|
|
//
|
|
|
|
// after:
|
|
// a
|
|
// \
|
|
// c
|
|
// \
|
|
// f
|
|
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
let nodeD = graph.addNode('d');
|
|
let nodeE = graph.addNode('e');
|
|
let nodeF = graph.addNode('f');
|
|
let nodeG = graph.addNode('g');
|
|
graph.setRootNodeId(nodeA);
|
|
|
|
graph.addEdge(nodeA, nodeB);
|
|
graph.addEdge(nodeA, nodeC);
|
|
graph.addEdge(nodeB, nodeD);
|
|
graph.addEdge(nodeG, nodeD);
|
|
graph.addEdge(nodeB, nodeE);
|
|
graph.addEdge(nodeC, nodeF);
|
|
graph.addEdge(nodeD, nodeG);
|
|
|
|
graph.removeNode(nodeB);
|
|
|
|
assert.deepEqual(graph.nodes.filter(Boolean), ['a', 'c', 'f']);
|
|
assert.deepEqual(Array.from(graph.getAllEdges()), [
|
|
{from: nodeA, to: nodeC, type: 1},
|
|
{from: nodeC, to: nodeF, type: 1},
|
|
]);
|
|
});
|
|
|
|
it('removing an edge to a node that cycles does not remove it if there is a path to the root', () => {
|
|
// a
|
|
// |
|
|
// b <----
|
|
// / \ |
|
|
// c d |
|
|
// \ / |
|
|
// e -----
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
let nodeD = graph.addNode('d');
|
|
let nodeE = graph.addNode('e');
|
|
graph.setRootNodeId(nodeA);
|
|
|
|
graph.addEdge(nodeA, nodeB);
|
|
graph.addEdge(nodeB, nodeC);
|
|
graph.addEdge(nodeB, nodeD);
|
|
graph.addEdge(nodeC, nodeE);
|
|
graph.addEdge(nodeD, nodeE);
|
|
graph.addEdge(nodeE, nodeB);
|
|
|
|
const getNodeIds = () => [...graph.nodes.keys()];
|
|
let nodesBefore = getNodeIds();
|
|
|
|
graph.removeEdge(nodeC, nodeE);
|
|
|
|
assert.deepEqual(nodesBefore, getNodeIds());
|
|
assert.deepEqual(Array.from(graph.getAllEdges()), [
|
|
{from: nodeA, to: nodeB, type: 1},
|
|
{from: nodeB, to: nodeC, type: 1},
|
|
{from: nodeB, to: nodeD, type: 1},
|
|
{from: nodeD, to: nodeE, type: 1},
|
|
{from: nodeE, to: nodeB, type: 1},
|
|
]);
|
|
});
|
|
|
|
it('removing a node with only one inbound edge does not cause it to be removed as an orphan', () => {
|
|
let graph = new Graph();
|
|
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
graph.setRootNodeId(nodeA);
|
|
|
|
graph.addEdge(nodeA, nodeB);
|
|
|
|
let spy = sinon.spy(graph, 'removeNode');
|
|
try {
|
|
graph.removeNode(nodeB);
|
|
|
|
assert(spy.calledOnceWithExactly(nodeB));
|
|
} finally {
|
|
spy.restore();
|
|
}
|
|
});
|
|
|
|
it("replaceNodeIdsConnectedTo should update a node's downstream nodes", () => {
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
graph.setRootNodeId(nodeA);
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
graph.addEdge(nodeA, nodeB);
|
|
graph.addEdge(nodeA, nodeC);
|
|
|
|
let nodeD = graph.addNode('d');
|
|
graph.replaceNodeIdsConnectedTo(nodeA, [nodeB, nodeD]);
|
|
|
|
assert(graph.hasNode(nodeA));
|
|
assert(graph.hasNode(nodeB));
|
|
assert(!graph.hasNode(nodeC));
|
|
assert(graph.hasNode(nodeD));
|
|
assert.deepEqual(Array.from(graph.getAllEdges()), [
|
|
{from: nodeA, to: nodeB, type: 1},
|
|
{from: nodeA, to: nodeD, type: 1},
|
|
]);
|
|
});
|
|
|
|
it('traverses along edge types if a filter is given', () => {
|
|
let graph = new Graph();
|
|
let nodeA = graph.addNode('a');
|
|
let nodeB = graph.addNode('b');
|
|
let nodeC = graph.addNode('c');
|
|
let nodeD = graph.addNode('d');
|
|
|
|
graph.addEdge(nodeA, nodeB, 2);
|
|
graph.addEdge(nodeA, nodeD);
|
|
graph.addEdge(nodeB, nodeC);
|
|
graph.addEdge(nodeB, nodeD, 2);
|
|
|
|
graph.setRootNodeId(nodeA);
|
|
|
|
let visited = [];
|
|
graph.traverse(
|
|
nodeId => {
|
|
visited.push(nodeId);
|
|
},
|
|
null, // use root as startNode
|
|
2,
|
|
);
|
|
|
|
assert.deepEqual(visited, [nodeA, nodeB, nodeD]);
|
|
});
|
|
|
|
it('correctly removes non-tree subgraphs', () => {
|
|
let graph = new Graph();
|
|
let nodeRoot = graph.addNode('root');
|
|
let node1 = graph.addNode('1');
|
|
let node2 = graph.addNode('2');
|
|
let node3 = graph.addNode('3');
|
|
|
|
graph.addEdge(nodeRoot, node1);
|
|
graph.addEdge(node1, node2);
|
|
graph.addEdge(node1, node3);
|
|
graph.addEdge(node2, node3);
|
|
|
|
graph.setRootNodeId(nodeRoot);
|
|
|
|
graph.removeNode(node1);
|
|
|
|
assert.deepEqual(graph.nodes.filter(Boolean), ['root']);
|
|
assert.deepStrictEqual(Array.from(graph.getAllEdges()), []);
|
|
});
|
|
});
|