Path: blob/main/build/lib/test/checkCyclicDependencies.test.ts
13383 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { suite, test, beforeEach, afterEach } from 'node:test';7import fs from 'fs';8import path from 'path';9import os from 'os';10import { Graph, collectJsFiles, processFile, normalize } from '../checkCyclicDependencies.ts';1112suite('checkCyclicDependencies', () => {1314suite('Graph', () => {1516test('no cycles in linear chain', () => {17const graph = new Graph();18graph.inertEdge('a', 'b');19graph.inertEdge('b', 'c');20const cycles = graph.findCycles(['a', 'b', 'c']);21for (const [, cycle] of cycles) {22assert.strictEqual(cycle, undefined);23}24});2526test('detects simple cycle', () => {27const graph = new Graph();28graph.inertEdge('a', 'b');29graph.inertEdge('b', 'a');30const cycles = graph.findCycles(['a', 'b']);31const hasCycle = Array.from(cycles.values()).some(c => c !== undefined);32assert.ok(hasCycle);33});3435test('detects 3-node cycle', () => {36const graph = new Graph();37graph.inertEdge('a', 'b');38graph.inertEdge('b', 'c');39graph.inertEdge('c', 'a');40const cycles = graph.findCycles(['a', 'b', 'c']);41const hasCycle = Array.from(cycles.values()).some(c => c !== undefined);42assert.ok(hasCycle);43});4445test('no false positives with shared dependencies', () => {46const graph = new Graph();47// diamond: a -> b, a -> c, b -> d, c -> d48graph.inertEdge('a', 'b');49graph.inertEdge('a', 'c');50graph.inertEdge('b', 'd');51graph.inertEdge('c', 'd');52const cycles = graph.findCycles(['a', 'b', 'c', 'd']);53for (const [, cycle] of cycles) {54assert.strictEqual(cycle, undefined);55}56});5758test('lookupOrInsertNode returns same node for same data', () => {59const graph = new Graph();60const node1 = graph.lookupOrInsertNode('x');61const node2 = graph.lookupOrInsertNode('x');62assert.strictEqual(node1, node2);63});6465test('lookup returns undefined for unknown node', () => {66const graph = new Graph();67assert.strictEqual(graph.lookup('unknown'), undefined);68});6970test('findCycles skips unknown data', () => {71const graph = new Graph();72graph.inertEdge('a', 'b');73const cycles = graph.findCycles(['nonexistent']);74assert.strictEqual(cycles.get('nonexistent'), undefined);75});7677test('cycle path contains the cycle nodes', () => {78const graph = new Graph();79graph.inertEdge('a', 'b');80graph.inertEdge('b', 'c');81graph.inertEdge('c', 'b');82const cycles = graph.findCycles(['a', 'b', 'c']);83const cyclePath = Array.from(cycles.values()).find(c => c !== undefined);84assert.ok(cyclePath);85assert.ok(cyclePath.includes('b'));86assert.ok(cyclePath.includes('c'));87// cycle should start and end with same node88assert.strictEqual(cyclePath[0], cyclePath[cyclePath.length - 1]);89});90});9192suite('normalize', () => {9394test('replaces backslashes with forward slashes', () => {95assert.strictEqual(normalize('a\\b\\c'), 'a/b/c');96});9798test('leaves forward slashes unchanged', () => {99assert.strictEqual(normalize('a/b/c'), 'a/b/c');100});101});102103suite('collectJsFiles and processFile', () => {104105let tmpDir: string;106107beforeEach(() => {108tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cyclic-test-'));109});110111afterEach(() => {112fs.rmSync(tmpDir, { recursive: true, force: true });113});114115test('collectJsFiles finds .js files recursively', () => {116fs.writeFileSync(path.join(tmpDir, 'a.js'), '');117fs.writeFileSync(path.join(tmpDir, 'b.ts'), '');118fs.mkdirSync(path.join(tmpDir, 'sub'));119fs.writeFileSync(path.join(tmpDir, 'sub', 'c.js'), '');120const files = collectJsFiles(tmpDir);121assert.strictEqual(files.length, 2);122assert.ok(files.some(f => f.endsWith('a.js')));123assert.ok(files.some(f => f.endsWith('c.js')));124});125126test('processFile adds edges for relative imports', () => {127fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import { x } from "./b";');128fs.writeFileSync(path.join(tmpDir, 'b.js'), '');129const graph = new Graph();130processFile(path.join(tmpDir, 'a.js'), graph);131const aNode = graph.lookup(normalize(path.join(tmpDir, 'a.js')));132assert.ok(aNode);133assert.strictEqual(aNode.outgoing.size, 1);134});135136test('processFile skips non-relative imports', () => {137fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import fs from "fs";');138const graph = new Graph();139processFile(path.join(tmpDir, 'a.js'), graph);140// no relative imports, so no edges and no node created141assert.strictEqual(graph.lookup(normalize(path.join(tmpDir, 'a.js'))), undefined);142});143144test('processFile skips CSS imports', () => {145fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import "./styles.css";');146const graph = new Graph();147processFile(path.join(tmpDir, 'a.js'), graph);148// CSS imports are ignored, so no edges and no node created149assert.strictEqual(graph.lookup(normalize(path.join(tmpDir, 'a.js'))), undefined);150});151152test('end-to-end: detects cycle in JS files', () => {153fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import { x } from "./b";');154fs.writeFileSync(path.join(tmpDir, 'b.js'), 'import { y } from "./a";');155const files = collectJsFiles(tmpDir);156const graph = new Graph();157for (const file of files) {158processFile(file, graph);159}160const allNormalized = files.map(normalize);161const cycles = graph.findCycles(allNormalized);162const hasCycle = Array.from(cycles.values()).some(c => c !== undefined);163assert.ok(hasCycle);164});165166test('end-to-end: no cycle in acyclic JS files', () => {167fs.writeFileSync(path.join(tmpDir, 'a.js'), 'import { x } from "./b";');168fs.writeFileSync(path.join(tmpDir, 'b.js'), '');169const files = collectJsFiles(tmpDir);170const graph = new Graph();171for (const file of files) {172processFile(file, graph);173}174const allNormalized = files.map(normalize);175const cycles = graph.findCycles(allNormalized);176for (const [, cycle] of cycles) {177assert.strictEqual(cycle, undefined);178}179});180});181});182183184