Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/test/checkCyclicDependencies.test.ts
13383 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { suite, test, beforeEach, afterEach } from 'node:test';
8
import fs from 'fs';
9
import path from 'path';
10
import os from 'os';
11
import { Graph, collectJsFiles, processFile, normalize } from '../checkCyclicDependencies.ts';
12
13
suite('checkCyclicDependencies', () => {
14
15
suite('Graph', () => {
16
17
test('no cycles in linear chain', () => {
18
const graph = new Graph();
19
graph.inertEdge('a', 'b');
20
graph.inertEdge('b', 'c');
21
const cycles = graph.findCycles(['a', 'b', 'c']);
22
for (const [, cycle] of cycles) {
23
assert.strictEqual(cycle, undefined);
24
}
25
});
26
27
test('detects simple cycle', () => {
28
const graph = new Graph();
29
graph.inertEdge('a', 'b');
30
graph.inertEdge('b', 'a');
31
const cycles = graph.findCycles(['a', 'b']);
32
const hasCycle = Array.from(cycles.values()).some(c => c !== undefined);
33
assert.ok(hasCycle);
34
});
35
36
test('detects 3-node cycle', () => {
37
const graph = new Graph();
38
graph.inertEdge('a', 'b');
39
graph.inertEdge('b', 'c');
40
graph.inertEdge('c', 'a');
41
const cycles = graph.findCycles(['a', 'b', 'c']);
42
const hasCycle = Array.from(cycles.values()).some(c => c !== undefined);
43
assert.ok(hasCycle);
44
});
45
46
test('no false positives with shared dependencies', () => {
47
const graph = new Graph();
48
// diamond: a -> b, a -> c, b -> d, c -> d
49
graph.inertEdge('a', 'b');
50
graph.inertEdge('a', 'c');
51
graph.inertEdge('b', 'd');
52
graph.inertEdge('c', 'd');
53
const cycles = graph.findCycles(['a', 'b', 'c', 'd']);
54
for (const [, cycle] of cycles) {
55
assert.strictEqual(cycle, undefined);
56
}
57
});
58
59
test('lookupOrInsertNode returns same node for same data', () => {
60
const graph = new Graph();
61
const node1 = graph.lookupOrInsertNode('x');
62
const node2 = graph.lookupOrInsertNode('x');
63
assert.strictEqual(node1, node2);
64
});
65
66
test('lookup returns undefined for unknown node', () => {
67
const graph = new Graph();
68
assert.strictEqual(graph.lookup('unknown'), undefined);
69
});
70
71
test('findCycles skips unknown data', () => {
72
const graph = new Graph();
73
graph.inertEdge('a', 'b');
74
const cycles = graph.findCycles(['nonexistent']);
75
assert.strictEqual(cycles.get('nonexistent'), undefined);
76
});
77
78
test('cycle path contains the cycle nodes', () => {
79
const graph = new Graph();
80
graph.inertEdge('a', 'b');
81
graph.inertEdge('b', 'c');
82
graph.inertEdge('c', 'b');
83
const cycles = graph.findCycles(['a', 'b', 'c']);
84
const cyclePath = Array.from(cycles.values()).find(c => c !== undefined);
85
assert.ok(cyclePath);
86
assert.ok(cyclePath.includes('b'));
87
assert.ok(cyclePath.includes('c'));
88
// cycle should start and end with same node
89
assert.strictEqual(cyclePath[0], cyclePath[cyclePath.length - 1]);
90
});
91
});
92
93
suite('normalize', () => {
94
95
test('replaces backslashes with forward slashes', () => {
96
assert.strictEqual(normalize('a\\b\\c'), 'a/b/c');
97
});
98
99
test('leaves forward slashes unchanged', () => {
100
assert.strictEqual(normalize('a/b/c'), 'a/b/c');
101
});
102
});
103
104
suite('collectJsFiles and processFile', () => {
105
106
let tmpDir: string;
107
108
beforeEach(() => {
109
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cyclic-test-'));
110
});
111
112
afterEach(() => {
113
fs.rmSync(tmpDir, { recursive: true, force: true });
114
});
115
116
test('collectJsFiles finds .js files recursively', () => {
117
fs.writeFileSync(path.join(tmpDir, 'a.js'), '');
118
fs.writeFileSync(path.join(tmpDir, 'b.ts'), '');
119
fs.mkdirSync(path.join(tmpDir, 'sub'));
120
fs.writeFileSync(path.join(tmpDir, 'sub', 'c.js'), '');
121
const files = collectJsFiles(tmpDir);
122
assert.strictEqual(files.length, 2);
123
assert.ok(files.some(f => f.endsWith('a.js')));
124
assert.ok(files.some(f => f.endsWith('c.js')));
125
});
126
127
test('processFile adds edges for relative imports', () => {
128
fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import { x } from "./b";');
129
fs.writeFileSync(path.join(tmpDir, 'b.js'), '');
130
const graph = new Graph();
131
processFile(path.join(tmpDir, 'a.js'), graph);
132
const aNode = graph.lookup(normalize(path.join(tmpDir, 'a.js')));
133
assert.ok(aNode);
134
assert.strictEqual(aNode.outgoing.size, 1);
135
});
136
137
test('processFile skips non-relative imports', () => {
138
fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import fs from "fs";');
139
const graph = new Graph();
140
processFile(path.join(tmpDir, 'a.js'), graph);
141
// no relative imports, so no edges and no node created
142
assert.strictEqual(graph.lookup(normalize(path.join(tmpDir, 'a.js'))), undefined);
143
});
144
145
test('processFile skips CSS imports', () => {
146
fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import "./styles.css";');
147
const graph = new Graph();
148
processFile(path.join(tmpDir, 'a.js'), graph);
149
// CSS imports are ignored, so no edges and no node created
150
assert.strictEqual(graph.lookup(normalize(path.join(tmpDir, 'a.js'))), undefined);
151
});
152
153
test('end-to-end: detects cycle in JS files', () => {
154
fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import { x } from "./b";');
155
fs.writeFileSync(path.join(tmpDir, 'b.js'), 'import { y } from "./a";');
156
const files = collectJsFiles(tmpDir);
157
const graph = new Graph();
158
for (const file of files) {
159
processFile(file, graph);
160
}
161
const allNormalized = files.map(normalize);
162
const cycles = graph.findCycles(allNormalized);
163
const hasCycle = Array.from(cycles.values()).some(c => c !== undefined);
164
assert.ok(hasCycle);
165
});
166
167
test('end-to-end: no cycle in acyclic JS files', () => {
168
fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import { x } from "./b";');
169
fs.writeFileSync(path.join(tmpDir, 'b.js'), '');
170
const files = collectJsFiles(tmpDir);
171
const graph = new Graph();
172
for (const file of files) {
173
processFile(file, graph);
174
}
175
const allNormalized = files.map(normalize);
176
const cycles = graph.findCycles(allNormalized);
177
for (const [, cycle] of cycles) {
178
assert.strictEqual(cycle, undefined);
179
}
180
});
181
});
182
});
183
184