@atlaspack/graph 3.2.1-canary.3354

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,559 @@
1
+ // @flow strict-local
2
+
3
+ import assert from 'assert';
4
+ import sinon from 'sinon';
5
+ import type {TraversalActions} from '@atlaspack/types-internal';
6
+
7
+ import Graph from '../src/Graph';
8
+ import {toNodeId, type NodeId} from '../src/types';
9
+
10
+ describe('Graph', () => {
11
+ it('constructor should initialize an empty graph', () => {
12
+ let graph = new Graph();
13
+ assert.deepEqual(graph.nodes, []);
14
+ assert.deepEqual([...graph.getAllEdges()], []);
15
+ });
16
+
17
+ it('addNode should add a node to the graph', () => {
18
+ let graph = new Graph();
19
+ let node = {};
20
+ let id = graph.addNode(node);
21
+ assert.equal(graph.getNode(id), node);
22
+ });
23
+
24
+ it('errors when traversing a graph with no root', () => {
25
+ let graph = new Graph();
26
+
27
+ assert.throws(() => {
28
+ graph.traverse(() => {});
29
+ }, /A start node is required to traverse/);
30
+ });
31
+
32
+ it("errors when traversing a graph with a startNode that doesn't belong", () => {
33
+ let graph = new Graph();
34
+
35
+ assert.throws(() => {
36
+ graph.traverse(() => {}, toNodeId(-1));
37
+ }, /Does not have node/);
38
+ });
39
+
40
+ it("errors if replaceNodeIdsConnectedTo is called with a node that doesn't belong", () => {
41
+ let graph = new Graph();
42
+ assert.throws(() => {
43
+ graph.replaceNodeIdsConnectedTo(toNodeId(-1), []);
44
+ }, /Does not have node/);
45
+ });
46
+
47
+ it("errors when adding an edge to a node that doesn't exist", () => {
48
+ let graph = new Graph();
49
+ let node = graph.addNode({});
50
+ assert.throws(() => {
51
+ graph.addEdge(node, toNodeId(-1));
52
+ }, /"to" node '-1' not found/);
53
+ });
54
+
55
+ it("errors when adding an edge from a node that doesn't exist", () => {
56
+ let graph = new Graph();
57
+ let node = graph.addNode({});
58
+ assert.throws(() => {
59
+ graph.addEdge(toNodeId(-1), node);
60
+ }, /"from" node '-1' not found/);
61
+ });
62
+
63
+ it('hasNode should return a boolean based on whether the node exists in the graph', () => {
64
+ let graph = new Graph();
65
+ let node = graph.addNode({});
66
+ assert(graph.hasNode(node));
67
+ assert(!graph.hasNode(toNodeId(-1)));
68
+ });
69
+
70
+ it('addEdge should add an edge to the graph', () => {
71
+ let graph = new Graph();
72
+ let nodeA = graph.addNode('a');
73
+ let nodeB = graph.addNode('b');
74
+ graph.addEdge(nodeA, nodeB);
75
+ assert(graph.hasEdge(nodeA, nodeB));
76
+ });
77
+
78
+ it('isOrphanedNode should return true or false if the node is orphaned or not', () => {
79
+ let graph = new Graph();
80
+ let rootNode = graph.addNode('root');
81
+ graph.setRootNodeId(rootNode);
82
+
83
+ let nodeA = graph.addNode('a');
84
+ let nodeB = graph.addNode('b');
85
+ let nodeC = graph.addNode('c');
86
+ graph.addEdge(rootNode, nodeB);
87
+ graph.addEdge(nodeB, nodeC, 1);
88
+ assert(graph.isOrphanedNode(nodeA));
89
+ assert(!graph.isOrphanedNode(nodeB));
90
+ assert(!graph.isOrphanedNode(nodeC));
91
+ });
92
+
93
+ it("removeEdge should throw if the edge doesn't exist", () => {
94
+ let graph = new Graph();
95
+ let nodeA = graph.addNode('a');
96
+ let nodeB = graph.addNode('b');
97
+
98
+ assert.throws(() => {
99
+ graph.removeEdge(nodeA, nodeB);
100
+ }, /Edge from 0 to 1 not found!/);
101
+ });
102
+
103
+ it('removeEdge should prune the graph at that edge', () => {
104
+ // a
105
+ // / \
106
+ // b - d
107
+ // /
108
+ // c
109
+ let graph = new Graph();
110
+ let nodeA = graph.addNode('a');
111
+ graph.setRootNodeId(nodeA);
112
+ let nodeB = graph.addNode('b');
113
+ let nodeC = graph.addNode('c');
114
+ let nodeD = graph.addNode('d');
115
+ graph.addEdge(nodeA, nodeB);
116
+ graph.addEdge(nodeA, nodeD);
117
+ graph.addEdge(nodeB, nodeC);
118
+ graph.addEdge(nodeB, nodeD);
119
+
120
+ graph.removeEdge(nodeA, nodeB);
121
+ assert(graph.hasNode(nodeA));
122
+ assert(graph.hasNode(nodeD));
123
+ assert(!graph.hasNode(nodeB));
124
+ assert(!graph.hasNode(nodeC));
125
+ assert.deepEqual(
126
+ [...graph.getAllEdges()],
127
+ [{from: nodeA, to: nodeD, type: 1}],
128
+ );
129
+ });
130
+
131
+ it('removing a node recursively deletes orphaned nodes', () => {
132
+ // before:
133
+ // a
134
+ // / \
135
+ // b c
136
+ // / \ \
137
+ // d e f
138
+ // /
139
+ // g
140
+ //
141
+
142
+ // after:
143
+ // a
144
+ // \
145
+ // c
146
+ // \
147
+ // f
148
+
149
+ let graph = new Graph();
150
+ let nodeA = graph.addNode('a');
151
+ graph.setRootNodeId(nodeA);
152
+ let nodeB = graph.addNode('b');
153
+ let nodeC = graph.addNode('c');
154
+ let nodeD = graph.addNode('d');
155
+ let nodeE = graph.addNode('e');
156
+ let nodeF = graph.addNode('f');
157
+ let nodeG = graph.addNode('g');
158
+
159
+ graph.addEdge(nodeA, nodeB);
160
+ graph.addEdge(nodeA, nodeC);
161
+ graph.addEdge(nodeB, nodeD);
162
+ graph.addEdge(nodeB, nodeE);
163
+ graph.addEdge(nodeC, nodeF);
164
+ graph.addEdge(nodeD, nodeG);
165
+
166
+ graph.removeNode(nodeB);
167
+
168
+ assert.deepEqual(graph.nodes.filter(Boolean), ['a', 'c', 'f']);
169
+ assert.deepEqual(Array.from(graph.getAllEdges()), [
170
+ {from: nodeA, to: nodeC, type: 1},
171
+ {from: nodeC, to: nodeF, type: 1},
172
+ ]);
173
+ });
174
+
175
+ it('removing a node recursively deletes orphaned nodes if there is no path to the root', () => {
176
+ // before:
177
+ // a
178
+ // / \
179
+ // b c
180
+ // / \ \
181
+ // |-d e f
182
+ // |/
183
+ // g
184
+ //
185
+
186
+ // after:
187
+ // a
188
+ // \
189
+ // c
190
+ // \
191
+ // f
192
+
193
+ let graph = new Graph();
194
+ let nodeA = graph.addNode('a');
195
+ let nodeB = graph.addNode('b');
196
+ let nodeC = graph.addNode('c');
197
+ let nodeD = graph.addNode('d');
198
+ let nodeE = graph.addNode('e');
199
+ let nodeF = graph.addNode('f');
200
+ let nodeG = graph.addNode('g');
201
+ graph.setRootNodeId(nodeA);
202
+
203
+ graph.addEdge(nodeA, nodeB);
204
+ graph.addEdge(nodeA, nodeC);
205
+ graph.addEdge(nodeB, nodeD);
206
+ graph.addEdge(nodeG, nodeD);
207
+ graph.addEdge(nodeB, nodeE);
208
+ graph.addEdge(nodeC, nodeF);
209
+ graph.addEdge(nodeD, nodeG);
210
+
211
+ graph.removeNode(nodeB);
212
+
213
+ assert.deepEqual(graph.nodes.filter(Boolean), ['a', 'c', 'f']);
214
+ assert.deepEqual(Array.from(graph.getAllEdges()), [
215
+ {from: nodeA, to: nodeC, type: 1},
216
+ {from: nodeC, to: nodeF, type: 1},
217
+ ]);
218
+ });
219
+
220
+ it('removing an edge to a node that cycles does not remove it if there is a path to the root', () => {
221
+ // a
222
+ // |
223
+ // b <----
224
+ // / \ |
225
+ // c d |
226
+ // \ / |
227
+ // e -----
228
+ let graph = new Graph();
229
+ let nodeA = graph.addNode('a');
230
+ let nodeB = graph.addNode('b');
231
+ let nodeC = graph.addNode('c');
232
+ let nodeD = graph.addNode('d');
233
+ let nodeE = graph.addNode('e');
234
+ graph.setRootNodeId(nodeA);
235
+
236
+ graph.addEdge(nodeA, nodeB);
237
+ graph.addEdge(nodeB, nodeC);
238
+ graph.addEdge(nodeB, nodeD);
239
+ graph.addEdge(nodeC, nodeE);
240
+ graph.addEdge(nodeD, nodeE);
241
+ graph.addEdge(nodeE, nodeB);
242
+
243
+ const getNodeIds = () => [...graph.nodes.keys()];
244
+ let nodesBefore = getNodeIds();
245
+
246
+ graph.removeEdge(nodeC, nodeE);
247
+
248
+ assert.deepEqual(nodesBefore, getNodeIds());
249
+ assert.deepEqual(Array.from(graph.getAllEdges()), [
250
+ {from: nodeA, to: nodeB, type: 1},
251
+ {from: nodeB, to: nodeC, type: 1},
252
+ {from: nodeB, to: nodeD, type: 1},
253
+ {from: nodeD, to: nodeE, type: 1},
254
+ {from: nodeE, to: nodeB, type: 1},
255
+ ]);
256
+ });
257
+
258
+ it('removing a node with only one inbound edge does not cause it to be removed as an orphan', () => {
259
+ let graph = new Graph();
260
+
261
+ let nodeA = graph.addNode('a');
262
+ let nodeB = graph.addNode('b');
263
+ graph.setRootNodeId(nodeA);
264
+
265
+ graph.addEdge(nodeA, nodeB);
266
+
267
+ let spy = sinon.spy(graph, 'removeNode');
268
+ try {
269
+ graph.removeNode(nodeB);
270
+
271
+ assert(spy.calledOnceWithExactly(nodeB));
272
+ } finally {
273
+ spy.restore();
274
+ }
275
+ });
276
+
277
+ it("replaceNodeIdsConnectedTo should update a node's downstream nodes", () => {
278
+ let graph = new Graph();
279
+ let nodeA = graph.addNode('a');
280
+ graph.setRootNodeId(nodeA);
281
+ let nodeB = graph.addNode('b');
282
+ let nodeC = graph.addNode('c');
283
+ graph.addEdge(nodeA, nodeB);
284
+ graph.addEdge(nodeA, nodeC);
285
+
286
+ let nodeD = graph.addNode('d');
287
+ graph.replaceNodeIdsConnectedTo(nodeA, [nodeB, nodeD]);
288
+
289
+ assert(graph.hasNode(nodeA));
290
+ assert(graph.hasNode(nodeB));
291
+ assert(!graph.hasNode(nodeC));
292
+ assert(graph.hasNode(nodeD));
293
+ assert.deepEqual(Array.from(graph.getAllEdges()), [
294
+ {from: nodeA, to: nodeB, type: 1},
295
+ {from: nodeA, to: nodeD, type: 1},
296
+ ]);
297
+ });
298
+
299
+ it('traverses along edge types if a filter is given', () => {
300
+ let graph = new Graph();
301
+ let nodeA = graph.addNode('a');
302
+ let nodeB = graph.addNode('b');
303
+ let nodeC = graph.addNode('c');
304
+ let nodeD = graph.addNode('d');
305
+
306
+ graph.addEdge(nodeA, nodeB, 2);
307
+ graph.addEdge(nodeA, nodeD);
308
+ graph.addEdge(nodeB, nodeC);
309
+ graph.addEdge(nodeB, nodeD, 2);
310
+
311
+ graph.setRootNodeId(nodeA);
312
+
313
+ let visited = [];
314
+ graph.traverse(
315
+ nodeId => {
316
+ visited.push(nodeId);
317
+ },
318
+ null, // use root as startNode
319
+ 2,
320
+ );
321
+
322
+ assert.deepEqual(visited, [nodeA, nodeB, nodeD]);
323
+ });
324
+
325
+ it('correctly removes non-tree subgraphs', () => {
326
+ let graph = new Graph();
327
+ let nodeRoot = graph.addNode('root');
328
+ let node1 = graph.addNode('1');
329
+ let node2 = graph.addNode('2');
330
+ let node3 = graph.addNode('3');
331
+
332
+ graph.addEdge(nodeRoot, node1);
333
+ graph.addEdge(node1, node2);
334
+ graph.addEdge(node1, node3);
335
+ graph.addEdge(node2, node3);
336
+
337
+ graph.setRootNodeId(nodeRoot);
338
+
339
+ graph.removeNode(node1);
340
+
341
+ assert.deepEqual(graph.nodes.filter(Boolean), ['root']);
342
+ assert.deepStrictEqual(Array.from(graph.getAllEdges()), []);
343
+ });
344
+
345
+ describe('dfs(...)', () => {
346
+ it(`throws if the graph is empty`, () => {
347
+ const graph = new Graph();
348
+ const visit = sinon.stub();
349
+ const getChildren = sinon.stub();
350
+ assert.throws(() => {
351
+ graph.dfs({
352
+ visit,
353
+ startNodeId: 0,
354
+ getChildren,
355
+ });
356
+ }, /Does not have node 0/);
357
+ });
358
+
359
+ it(`visits a single node`, () => {
360
+ const graph = new Graph();
361
+ graph.addNode('root');
362
+ const visit = sinon.stub();
363
+ const getChildren = () => [];
364
+ graph.dfs({
365
+ visit,
366
+ startNodeId: 0,
367
+ getChildren,
368
+ });
369
+
370
+ assert(visit.calledOnce);
371
+ });
372
+
373
+ it(`visits all connected nodes in DFS order`, () => {
374
+ const graph = new Graph();
375
+ graph.addNode('0');
376
+ graph.addNode('1');
377
+ graph.addNode('2');
378
+ graph.addNode('3');
379
+ graph.addNode('disconnected-1');
380
+ graph.addNode('disconnected-2');
381
+ graph.addEdge(0, 1);
382
+ graph.addEdge(0, 2);
383
+ graph.addEdge(1, 3);
384
+ graph.addEdge(2, 3);
385
+
386
+ const order = [];
387
+ const visit = (node: NodeId) => {
388
+ order.push(node);
389
+ };
390
+ const getChildren = (node: NodeId) => graph.getNodeIdsConnectedFrom(node);
391
+ graph.dfs({
392
+ visit,
393
+ startNodeId: 0,
394
+ getChildren,
395
+ });
396
+
397
+ assert.deepEqual(order, [0, 1, 3, 2]);
398
+ });
399
+
400
+ describe(`actions tests`, () => {
401
+ it(`skips children if skip is called on a node`, () => {
402
+ const graph = new Graph();
403
+ graph.addNode('0');
404
+ graph.addNode('1');
405
+ graph.addNode('2');
406
+ graph.addNode('3');
407
+ graph.addNode('disconnected-1');
408
+ graph.addNode('disconnected-2');
409
+ graph.addEdge(0, 1);
410
+ graph.addEdge(1, 2);
411
+ graph.addEdge(0, 3);
412
+
413
+ const order = [];
414
+ const visit = (
415
+ node: NodeId,
416
+ context: mixed | null,
417
+ actions: TraversalActions,
418
+ ) => {
419
+ if (node === 1) actions.skipChildren();
420
+ order.push(node);
421
+ };
422
+ const getChildren = (node: NodeId) =>
423
+ graph.getNodeIdsConnectedFrom(node);
424
+ graph.dfs({
425
+ visit,
426
+ startNodeId: 0,
427
+ getChildren,
428
+ });
429
+
430
+ assert.deepEqual(order, [0, 1, 3]);
431
+ });
432
+
433
+ it(`stops the traversal if stop is called`, () => {
434
+ const graph = new Graph();
435
+ graph.addNode('0');
436
+ graph.addNode('1');
437
+ graph.addNode('2');
438
+ graph.addNode('3');
439
+ graph.addNode('disconnected-1');
440
+ graph.addNode('disconnected-2');
441
+ graph.addEdge(0, 1);
442
+ graph.addEdge(1, 2);
443
+ graph.addEdge(1, 3);
444
+ graph.addEdge(0, 2);
445
+ graph.addEdge(2, 3);
446
+
447
+ const order = [];
448
+ const visit = (
449
+ node: NodeId,
450
+ context: mixed | null,
451
+ actions: TraversalActions,
452
+ ) => {
453
+ order.push(node);
454
+ if (node === 1) {
455
+ actions.stop();
456
+ return 'result';
457
+ }
458
+ return 'other';
459
+ };
460
+ const getChildren = (node: NodeId) =>
461
+ graph.getNodeIdsConnectedFrom(node);
462
+ const result = graph.dfs({
463
+ visit,
464
+ startNodeId: 0,
465
+ getChildren,
466
+ });
467
+
468
+ assert.deepEqual(order, [0, 1]);
469
+ assert.equal(result, 'result');
470
+ });
471
+ });
472
+
473
+ describe(`context tests`, () => {
474
+ it(`passes the context between visitors`, () => {
475
+ const graph = new Graph();
476
+ graph.addNode('0');
477
+ graph.addNode('1');
478
+ graph.addNode('2');
479
+ graph.addNode('3');
480
+ graph.addNode('disconnected-1');
481
+ graph.addNode('disconnected-2');
482
+ graph.addEdge(0, 1);
483
+ graph.addEdge(1, 2);
484
+ graph.addEdge(1, 3);
485
+ graph.addEdge(0, 2);
486
+ graph.addEdge(2, 3);
487
+
488
+ const contexts = [];
489
+ const visit = (node: NodeId, context: mixed | null) => {
490
+ contexts.push([node, context]);
491
+ return `node-${node}-created-context`;
492
+ };
493
+ const getChildren = (node: NodeId) =>
494
+ graph.getNodeIdsConnectedFrom(node);
495
+ const result = graph.dfs({
496
+ visit,
497
+ startNodeId: 0,
498
+ getChildren,
499
+ });
500
+
501
+ assert.deepEqual(contexts, [
502
+ [0, undefined],
503
+ [1, 'node-0-created-context'],
504
+ [2, 'node-1-created-context'],
505
+ [3, 'node-2-created-context'],
506
+ ]);
507
+ assert.equal(result, undefined);
508
+ });
509
+ });
510
+
511
+ describe(`exit visitor tests`, () => {
512
+ it(`calls the exit visitor`, () => {
513
+ const graph = new Graph();
514
+ graph.addNode('0');
515
+ graph.addNode('1');
516
+ graph.addNode('2');
517
+ graph.addNode('3');
518
+ graph.addNode('disconnected-1');
519
+ graph.addNode('disconnected-2');
520
+ graph.addEdge(0, 1);
521
+ graph.addEdge(1, 2);
522
+ graph.addEdge(1, 3);
523
+ graph.addEdge(0, 2);
524
+
525
+ const contexts = [];
526
+ const visit = (node: NodeId, context: mixed | null) => {
527
+ contexts.push([node, context]);
528
+ return `node-${node}-created-context`;
529
+ };
530
+ const visitExit = (node: NodeId, context: mixed | null) => {
531
+ contexts.push(['exit', node, context]);
532
+ return `node-exit-${node}-created-context`;
533
+ };
534
+ const getChildren = (node: NodeId) =>
535
+ graph.getNodeIdsConnectedFrom(node);
536
+ const result = graph.dfs({
537
+ visit: {
538
+ enter: visit,
539
+ exit: visitExit,
540
+ },
541
+ startNodeId: 0,
542
+ getChildren,
543
+ });
544
+
545
+ assert.deepEqual(contexts, [
546
+ [0, undefined],
547
+ [1, 'node-0-created-context'],
548
+ [2, 'node-1-created-context'],
549
+ ['exit', 2, 'node-2-created-context'],
550
+ [3, 'node-1-created-context'],
551
+ ['exit', 3, 'node-3-created-context'],
552
+ ['exit', 1, 'node-1-created-context'],
553
+ ['exit', 0, 'node-0-created-context'],
554
+ ]);
555
+ assert.equal(result, undefined);
556
+ });
557
+ });
558
+ });
559
+ });
@@ -0,0 +1,20 @@
1
+ require('@atlaspack/babel-register');
2
+ const {parentPort} = require('worker_threads');
3
+ const {
4
+ default: AdjacencyList,
5
+ NodeTypeMap,
6
+ EdgeTypeMap,
7
+ } = require('../../src/AdjacencyList');
8
+
9
+ parentPort.once('message', serialized => {
10
+ let graph = AdjacencyList.deserialize(serialized);
11
+ serialized.nodes.forEach((v, i) => {
12
+ if (i < NodeTypeMap.HEADER_SIZE) return;
13
+ serialized.nodes[i] = v * 2;
14
+ });
15
+ serialized.edges.forEach((v, i) => {
16
+ if (i < EdgeTypeMap.HEADER_SIZE) return;
17
+ serialized.edges[i] = v * 2;
18
+ });
19
+ parentPort.postMessage(graph.serialize());
20
+ });