1 |
|
2 |
|
3 | import assert from 'assert';
|
4 | import sinon from 'sinon';
|
5 |
|
6 | import Graph from '../src/Graph';
|
7 |
|
8 | describe('Graph', () => {
|
9 | it('constructor should initialize an empty graph', () => {
|
10 | let graph = new Graph();
|
11 | assert.deepEqual(graph.nodes, new Map());
|
12 | assert.deepEqual(graph.getAllEdges(), []);
|
13 | });
|
14 |
|
15 | it('addNode should add a node to the graph', () => {
|
16 | let graph = new Graph();
|
17 | let node = {id: 'a', value: 'a'};
|
18 | graph.addNode(node);
|
19 | assert.equal(graph.nodes.get(node.id), node);
|
20 | });
|
21 |
|
22 | it("errors when removeNode is called with a node that doesn't belong", () => {
|
23 | let graph = new Graph();
|
24 | assert.throws(() => {
|
25 | graph.removeNode({id: 'dne', value: null});
|
26 | }, /Does not have node/);
|
27 | });
|
28 |
|
29 | it('errors when traversing a graph with no root', () => {
|
30 | let graph = new Graph();
|
31 |
|
32 | assert.throws(() => {
|
33 | graph.traverse(() => {});
|
34 | }, /A start node is required to traverse/);
|
35 | });
|
36 |
|
37 | it("errors when traversing a graph with a startNode that doesn't belong", () => {
|
38 | let graph = new Graph();
|
39 |
|
40 | assert.throws(() => {
|
41 | graph.traverse(() => {}, {id: 'dne', value: null});
|
42 | }, /Does not have node/);
|
43 | });
|
44 |
|
45 | it("errors if replaceNodesConnectedTo is called with a node that doesn't belong", () => {
|
46 | let graph = new Graph();
|
47 | assert.throws(() => {
|
48 | graph.replaceNodesConnectedTo({id: 'dne', value: null}, []);
|
49 | }, /Does not have node/);
|
50 | });
|
51 |
|
52 | it("errors when adding an edge to a node that doesn't exist", () => {
|
53 | let graph = new Graph();
|
54 | graph.addNode({id: 'foo', value: null});
|
55 | assert.throws(() => {
|
56 | graph.addEdge('foo', 'dne');
|
57 | }, /"to" node 'dne' not found/);
|
58 | });
|
59 |
|
60 | it("errors when adding an edge from a node that doesn't exist", () => {
|
61 | let graph = new Graph();
|
62 | graph.addNode({id: 'foo', value: null});
|
63 | assert.throws(() => {
|
64 | graph.addEdge('dne', 'foo');
|
65 | }, /"from" node 'dne' not found/);
|
66 | });
|
67 |
|
68 | it('hasNode should return a boolean based on whether the node exists in the graph', () => {
|
69 | let graph = new Graph();
|
70 | let node = {id: 'a', value: 'a'};
|
71 | graph.addNode(node);
|
72 | assert(graph.hasNode(node.id));
|
73 | assert(!graph.hasNode('b'));
|
74 | });
|
75 |
|
76 | it('addEdge should add an edge to the graph', () => {
|
77 | let graph = new Graph();
|
78 | graph.addNode({id: 'a', value: null});
|
79 | graph.addNode({id: 'b', value: null});
|
80 | graph.addEdge('a', 'b');
|
81 | assert(graph.hasEdge('a', 'b'));
|
82 | });
|
83 |
|
84 | it('isOrphanedNode should return true or false if the node is orphaned or not', () => {
|
85 | let graph = new Graph();
|
86 | let nodeA = {id: 'a', value: 'a'};
|
87 | let nodeB = {id: 'b', value: 'b'};
|
88 | let nodeC = {id: 'c', value: 'c'};
|
89 | graph.addNode(nodeA);
|
90 | graph.addNode(nodeB);
|
91 | graph.addNode(nodeC);
|
92 | graph.addEdge('a', 'b');
|
93 | graph.addEdge('a', 'c', 'edgetype');
|
94 | assert(graph.isOrphanedNode(nodeA));
|
95 | assert(!graph.isOrphanedNode(nodeB));
|
96 | assert(!graph.isOrphanedNode(nodeC));
|
97 | });
|
98 |
|
99 | it('removeEdge should prune the graph at that edge', () => {
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | let graph = new Graph();
|
106 | graph.addNode({id: 'a', value: 'a'});
|
107 | graph.addNode({id: 'b', value: 'b'});
|
108 | graph.addNode({id: 'c', value: 'c'});
|
109 | graph.addNode({id: 'd', value: 'd'});
|
110 | graph.addEdge('a', 'b');
|
111 | graph.addEdge('a', 'd');
|
112 | graph.addEdge('b', 'c');
|
113 | graph.addEdge('b', 'd');
|
114 |
|
115 | graph.removeEdge('a', 'b');
|
116 | assert(graph.nodes.has('a'));
|
117 | assert(graph.nodes.has('d'));
|
118 | assert(!graph.nodes.has('b'));
|
119 | assert(!graph.nodes.has('c'));
|
120 | assert.deepEqual(graph.getAllEdges(), [{from: 'a', to: 'd', type: null}]);
|
121 | });
|
122 |
|
123 | it('removing a node recursively deletes orphaned nodes', () => {
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | let graph = new Graph();
|
142 | graph.addNode({id: 'a', value: 'a'});
|
143 | graph.addNode({id: 'b', value: 'b'});
|
144 | graph.addNode({id: 'c', value: 'c'});
|
145 | graph.addNode({id: 'd', value: 'd'});
|
146 | graph.addNode({id: 'e', value: 'e'});
|
147 | graph.addNode({id: 'f', value: 'f'});
|
148 | graph.addNode({id: 'g', value: 'g'});
|
149 |
|
150 | graph.addEdge('a', 'b');
|
151 | graph.addEdge('a', 'c');
|
152 | graph.addEdge('b', 'd');
|
153 | graph.addEdge('b', 'e');
|
154 | graph.addEdge('c', 'f');
|
155 | graph.addEdge('d', 'g');
|
156 |
|
157 | graph.removeById('b');
|
158 |
|
159 | assert.deepEqual([...graph.nodes.values()].map(node => node.id), [
|
160 | 'a',
|
161 | 'c',
|
162 | 'f'
|
163 | ]);
|
164 | assert.deepEqual(graph.getAllEdges(), [
|
165 | {from: 'a', to: 'c', type: null},
|
166 | {from: 'c', to: 'f', type: null}
|
167 | ]);
|
168 | });
|
169 |
|
170 | it('removing a node recursively deletes orphaned nodes if there is no path to the root', () => {
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | let graph = new Graph();
|
189 | graph.setRootNode({id: 'a', value: 'a'});
|
190 | graph.addNode({id: 'b', value: 'b'});
|
191 | graph.addNode({id: 'c', value: 'c'});
|
192 | graph.addNode({id: 'd', value: 'd'});
|
193 | graph.addNode({id: 'e', value: 'e'});
|
194 | graph.addNode({id: 'f', value: 'f'});
|
195 | graph.addNode({id: 'g', value: 'g'});
|
196 |
|
197 | graph.addEdge('a', 'b');
|
198 | graph.addEdge('a', 'c');
|
199 | graph.addEdge('b', 'd');
|
200 | graph.addEdge('g', 'd');
|
201 | graph.addEdge('b', 'e');
|
202 | graph.addEdge('c', 'f');
|
203 | graph.addEdge('d', 'g');
|
204 |
|
205 | graph.removeById('b');
|
206 |
|
207 | assert.deepEqual([...graph.nodes.values()].map(node => node.id), [
|
208 | 'a',
|
209 | 'c',
|
210 | 'f'
|
211 | ]);
|
212 | assert.deepEqual(graph.getAllEdges(), [
|
213 | {from: 'a', to: 'c', type: null},
|
214 | {from: 'c', to: 'f', type: null}
|
215 | ]);
|
216 | });
|
217 |
|
218 | it('removing an edge to a node that cycles does not remove it if there is a path to the root', () => {
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | let graph = new Graph();
|
227 | graph.setRootNode({id: 'a', value: 'a'});
|
228 | graph.addNode({id: 'b', value: 'b'});
|
229 | graph.addNode({id: 'c', value: 'c'});
|
230 | graph.addNode({id: 'd', value: 'd'});
|
231 | graph.addNode({id: 'e', value: 'e'});
|
232 |
|
233 | graph.addEdge('a', 'b');
|
234 | graph.addEdge('b', 'c');
|
235 | graph.addEdge('b', 'd');
|
236 | graph.addEdge('c', 'e');
|
237 | graph.addEdge('d', 'e');
|
238 | graph.addEdge('e', 'b');
|
239 |
|
240 | const getNodeIds = () => [...graph.nodes.values()].map(node => node.id);
|
241 | let nodesBefore = getNodeIds();
|
242 |
|
243 | graph.removeEdge('c', 'e');
|
244 |
|
245 | assert.deepEqual(nodesBefore, getNodeIds());
|
246 | assert.deepEqual(graph.getAllEdges(), [
|
247 | {from: 'a', to: 'b', type: null},
|
248 | {from: 'b', to: 'c', type: null},
|
249 | {from: 'b', to: 'd', type: null},
|
250 | {from: 'd', to: 'e', type: null},
|
251 | {from: 'e', to: 'b', type: null}
|
252 | ]);
|
253 | });
|
254 |
|
255 | it('removing a node with only one inbound edge does not cause it to be removed as an orphan', () => {
|
256 | let graph = new Graph();
|
257 |
|
258 | graph.setRootNode({id: 'a', value: 'a'});
|
259 | graph.addNode({id: 'b', value: 'b'});
|
260 | graph.addEdge('a', 'b');
|
261 |
|
262 | let spy = sinon.spy(graph, 'removeNode');
|
263 | try {
|
264 | graph.removeById('b');
|
265 |
|
266 | assert(spy.calledOnceWithExactly({id: 'b', value: 'b'}));
|
267 | } finally {
|
268 | spy.restore();
|
269 | }
|
270 | });
|
271 |
|
272 | it("replaceNodesConnectedTo should update a node's downstream nodes", () => {
|
273 | let graph = new Graph();
|
274 | let nodeA = graph.addNode({id: 'a', value: 'a'});
|
275 | let nodeB = graph.addNode({id: 'b', value: 'b'});
|
276 | graph.addNode({id: 'c', value: 'c'});
|
277 | graph.addEdge('a', 'b');
|
278 | graph.addEdge('a', 'c');
|
279 |
|
280 | let nodeD = {id: 'd', value: 'd'};
|
281 | graph.replaceNodesConnectedTo(nodeA, [nodeB, nodeD]);
|
282 |
|
283 | assert(graph.nodes.has('a'));
|
284 | assert(graph.nodes.has('b'));
|
285 | assert(!graph.nodes.has('c'));
|
286 | assert(graph.nodes.has('d'));
|
287 | assert.deepEqual(graph.getAllEdges(), [
|
288 | {from: 'a', to: 'b', type: null},
|
289 | {from: 'a', to: 'd', type: null}
|
290 | ]);
|
291 | });
|
292 |
|
293 | it('traverses along edge types if a filter is given', () => {
|
294 | let graph = new Graph();
|
295 | graph.addNode({id: 'a', value: 'a'});
|
296 | graph.addNode({id: 'b', value: 'b'});
|
297 | graph.addNode({id: 'c', value: 'c'});
|
298 | graph.addNode({id: 'd', value: 'd'});
|
299 |
|
300 | graph.addEdge('a', 'b', 'edgetype');
|
301 | graph.addEdge('a', 'd');
|
302 | graph.addEdge('b', 'c');
|
303 | graph.addEdge('b', 'd', 'edgetype');
|
304 |
|
305 | graph.rootNodeId = 'a';
|
306 |
|
307 | let visited = [];
|
308 | graph.traverse(
|
309 | node => {
|
310 | visited.push(node.id);
|
311 | },
|
312 | null,
|
313 | 'edgetype'
|
314 | );
|
315 | assert.deepEqual(visited, ['a', 'b', 'd']);
|
316 | });
|
317 | });
|