Skip to content

Commit 107e4b1

Browse files
Add comprehensive tests for gitignore utility functions
Introduce unit and integration tests to validate gitignore utility functions, ensuring proper parsing, matching, and exclusion logic. These tests improve code reliability and prevent regressions. - Added tests for `parseGitignoreContent` to verify pattern parsing. - Tested `matchesGitignorePatterns` to validate inclusion/exclusion logic. - Implemented integration tests to check filesystem interactions. - Verified `collectGitignorePatterns` and `shouldExcludeByGitignore` behaviors. - Confirmed `buildTreeNode` respects gitignore patterns accurately.
1 parent 43af36f commit 107e4b1

File tree

2 files changed

+346
-0
lines changed

2 files changed

+346
-0
lines changed

src/gitignore-utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ function matchesPattern(
9797
): boolean {
9898
const patternStr = pattern.pattern.replace(/\\/g, '/');
9999

100+
// For directory patterns (ending with /), also check without the trailing slash
101+
// when matching against directories
102+
if (pattern.isDirectory && isDirectory && patternStr.endsWith('/')) {
103+
const patternWithoutSlash = patternStr.slice(0, -1);
104+
if (normalizedPath === patternWithoutSlash) {
105+
return true;
106+
}
107+
}
108+
100109
// Handle exact matches
101110
if (!patternStr.includes('*')) {
102111
if (pattern.isAbsolute) {

src/test/gitignore-utils.test.ts

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import * as assert from 'assert';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
import * as os from 'os';
5+
import {
6+
parseGitignoreContent,
7+
parseGitignoreFile,
8+
matchesGitignorePatterns,
9+
collectDirectoryGitignorePatterns,
10+
collectGitignorePatterns,
11+
shouldExcludeByGitignore,
12+
GitignorePattern
13+
} from '../gitignore-utils';
14+
import { buildTreeNode } from '../extension';
15+
16+
suite('Gitignore Utils Test Suite', () => {
17+
suite('parseGitignoreContent', () => {
18+
test('parses empty content correctly', () => {
19+
const patterns = parseGitignoreContent('');
20+
assert.strictEqual(patterns.length, 0, 'Empty content should result in empty patterns array');
21+
});
22+
23+
test('parses basic patterns correctly', () => {
24+
const content = `
25+
# This is a comment
26+
node_modules
27+
*.log
28+
!important.log
29+
/dist
30+
build/
31+
`;
32+
const patterns = parseGitignoreContent(content);
33+
34+
assert.strictEqual(patterns.length, 5, 'Should parse 5 patterns');
35+
36+
// Check node_modules pattern
37+
const nodeModulesPattern = patterns.find(p => p.pattern === 'node_modules');
38+
assert.ok(nodeModulesPattern, 'node_modules pattern should exist');
39+
assert.strictEqual(nodeModulesPattern!.isNegated, false, 'node_modules should not be negated');
40+
assert.strictEqual(nodeModulesPattern!.isDirectory, false, 'node_modules should not be marked as directory');
41+
assert.strictEqual(nodeModulesPattern!.isAbsolute, false, 'node_modules should not be absolute');
42+
43+
// Check *.log pattern
44+
const logPattern = patterns.find(p => p.pattern === '*.log');
45+
assert.ok(logPattern, '*.log pattern should exist');
46+
assert.strictEqual(logPattern!.isNegated, false, '*.log should not be negated');
47+
48+
// Check !important.log pattern
49+
const importantLogPattern = patterns.find(p => p.pattern === 'important.log');
50+
assert.ok(importantLogPattern, 'important.log pattern should exist');
51+
assert.strictEqual(importantLogPattern!.isNegated, true, 'important.log should be negated');
52+
53+
// Check /dist pattern
54+
const distPattern = patterns.find(p => p.pattern === 'dist');
55+
assert.ok(distPattern, 'dist pattern should exist');
56+
assert.strictEqual(distPattern!.isAbsolute, true, 'dist should be absolute');
57+
58+
// Check build/ pattern
59+
const buildPattern = patterns.find(p => p.pattern === 'build/');
60+
assert.ok(buildPattern, 'build/ pattern should exist');
61+
assert.strictEqual(buildPattern!.isDirectory, true, 'build/ should be marked as directory');
62+
});
63+
});
64+
65+
suite('matchesGitignorePatterns', () => {
66+
test('matches basic patterns correctly', () => {
67+
const patterns: GitignorePattern[] = [
68+
{ pattern: 'node_modules', isNegated: false, isDirectory: false, isAbsolute: false },
69+
{ pattern: '*.log', isNegated: false, isDirectory: false, isAbsolute: false },
70+
{ pattern: 'important.log', isNegated: true, isDirectory: false, isAbsolute: false },
71+
{ pattern: 'dist', isNegated: false, isDirectory: false, isAbsolute: true },
72+
{ pattern: 'build/', isNegated: false, isDirectory: true, isAbsolute: false }
73+
];
74+
75+
// Should match
76+
assert.strictEqual(
77+
matchesGitignorePatterns('node_modules', patterns, false),
78+
true,
79+
'node_modules should match'
80+
);
81+
82+
assert.strictEqual(
83+
matchesGitignorePatterns('logs/error.log', patterns, false),
84+
true,
85+
'*.log should match error.log'
86+
);
87+
88+
assert.strictEqual(
89+
matchesGitignorePatterns('dist', patterns, false),
90+
true,
91+
'dist should match'
92+
);
93+
94+
assert.strictEqual(
95+
matchesGitignorePatterns('build', patterns, true),
96+
true,
97+
'build/ should match build directory'
98+
);
99+
100+
// Should not match
101+
assert.strictEqual(
102+
matchesGitignorePatterns('important.log', patterns, false),
103+
false,
104+
'important.log should not match due to negation'
105+
);
106+
107+
assert.strictEqual(
108+
matchesGitignorePatterns('src', patterns, false),
109+
false,
110+
'src should not match any pattern'
111+
);
112+
113+
assert.strictEqual(
114+
matchesGitignorePatterns('build', patterns, false),
115+
false,
116+
'build/ should not match build file (only directory)'
117+
);
118+
});
119+
});
120+
121+
suite('Integration with file system', () => {
122+
let tmpDir: string;
123+
124+
suiteSetup(() => {
125+
// Create a temp directory
126+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitignore-test-'));
127+
128+
// Create a .gitignore file
129+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), `
130+
# Test gitignore file
131+
node_modules
132+
*.log
133+
!important.log
134+
/dist
135+
build/
136+
`);
137+
138+
// Create some files and directories
139+
fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
140+
fs.writeFileSync(path.join(tmpDir, 'error.log'), 'error');
141+
fs.writeFileSync(path.join(tmpDir, 'important.log'), 'important');
142+
143+
fs.mkdirSync(path.join(tmpDir, 'src'));
144+
fs.writeFileSync(path.join(tmpDir, 'src', 'index.js'), 'console.log("Hello")');
145+
146+
fs.mkdirSync(path.join(tmpDir, 'dist'));
147+
fs.writeFileSync(path.join(tmpDir, 'dist', 'bundle.js'), 'bundled code');
148+
149+
fs.mkdirSync(path.join(tmpDir, 'build'));
150+
fs.writeFileSync(path.join(tmpDir, 'build', 'output.js'), 'output');
151+
152+
fs.mkdirSync(path.join(tmpDir, 'node_modules'));
153+
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'package.json'), '{}');
154+
155+
// Create a subdirectory with its own .gitignore
156+
fs.mkdirSync(path.join(tmpDir, 'subdir'));
157+
fs.writeFileSync(path.join(tmpDir, 'subdir', '.gitignore'), `
158+
# Subdir gitignore
159+
*.txt
160+
!important.txt
161+
`);
162+
fs.writeFileSync(path.join(tmpDir, 'subdir', 'file.txt'), 'content');
163+
fs.writeFileSync(path.join(tmpDir, 'subdir', 'important.txt'), 'important');
164+
fs.writeFileSync(path.join(tmpDir, 'subdir', 'code.js'), 'code');
165+
});
166+
167+
suiteTeardown(() => {
168+
// Clean up
169+
fs.rmSync(tmpDir, { recursive: true, force: true });
170+
});
171+
172+
test('parseGitignoreFile reads file correctly', () => {
173+
const patterns = parseGitignoreFile(path.join(tmpDir, '.gitignore'));
174+
assert.ok(patterns.length > 0, 'Should parse patterns from file');
175+
assert.ok(patterns.some(p => p.pattern === 'node_modules'), 'Should include node_modules pattern');
176+
});
177+
178+
test('collectDirectoryGitignorePatterns collects patterns from directory', () => {
179+
const patterns = collectDirectoryGitignorePatterns(tmpDir);
180+
assert.ok(patterns.length > 0, 'Should collect patterns from directory');
181+
assert.ok(patterns.some(p => p.pattern === 'node_modules'), 'Should include node_modules pattern');
182+
});
183+
184+
test('collectGitignorePatterns collects patterns from directory and parents', () => {
185+
const patterns = collectGitignorePatterns(path.join(tmpDir, 'subdir'));
186+
assert.ok(patterns.length > 0, 'Should collect patterns from directory and parents');
187+
assert.ok(patterns.some(p => p.pattern === '*.txt'), 'Should include *.txt pattern from subdir');
188+
});
189+
190+
test('shouldExcludeByGitignore correctly identifies excluded files', () => {
191+
// Files that should be excluded
192+
assert.strictEqual(
193+
shouldExcludeByGitignore(
194+
path.join(tmpDir, 'node_modules'),
195+
tmpDir,
196+
collectDirectoryGitignorePatterns(tmpDir),
197+
true
198+
),
199+
true,
200+
'node_modules directory should be excluded'
201+
);
202+
203+
assert.strictEqual(
204+
shouldExcludeByGitignore(
205+
path.join(tmpDir, 'error.log'),
206+
tmpDir,
207+
collectDirectoryGitignorePatterns(tmpDir),
208+
false
209+
),
210+
true,
211+
'error.log should be excluded'
212+
);
213+
214+
assert.strictEqual(
215+
shouldExcludeByGitignore(
216+
path.join(tmpDir, 'dist'),
217+
tmpDir,
218+
collectDirectoryGitignorePatterns(tmpDir),
219+
true
220+
),
221+
true,
222+
'dist directory should be excluded'
223+
);
224+
225+
assert.strictEqual(
226+
shouldExcludeByGitignore(
227+
path.join(tmpDir, 'build'),
228+
tmpDir,
229+
collectDirectoryGitignorePatterns(tmpDir),
230+
true
231+
),
232+
true,
233+
'build directory should be excluded'
234+
);
235+
236+
// Files that should not be excluded
237+
assert.strictEqual(
238+
shouldExcludeByGitignore(
239+
path.join(tmpDir, 'important.log'),
240+
tmpDir,
241+
collectDirectoryGitignorePatterns(tmpDir),
242+
false
243+
),
244+
false,
245+
'important.log should not be excluded due to negation'
246+
);
247+
248+
assert.strictEqual(
249+
shouldExcludeByGitignore(
250+
path.join(tmpDir, 'file.txt'),
251+
tmpDir,
252+
collectDirectoryGitignorePatterns(tmpDir),
253+
false
254+
),
255+
false,
256+
'file.txt should not be excluded'
257+
);
258+
259+
assert.strictEqual(
260+
shouldExcludeByGitignore(
261+
path.join(tmpDir, 'src'),
262+
tmpDir,
263+
collectDirectoryGitignorePatterns(tmpDir),
264+
true
265+
),
266+
false,
267+
'src directory should not be excluded'
268+
);
269+
});
270+
271+
test('buildTreeNode respects gitignore patterns', () => {
272+
const tree = buildTreeNode(tmpDir);
273+
274+
// Check that the tree has the correct structure
275+
assert.strictEqual(tree.name, path.basename(tmpDir), 'Root node should have correct name');
276+
assert.strictEqual(tree.type, 'folder', 'Root node should be a folder');
277+
278+
// Find nodes for various files/directories
279+
const findNode = (name: string) => {
280+
return tree.children!.find(node => node.name === name);
281+
};
282+
283+
// Files/directories that should be included but unchecked
284+
const nodeModulesNode = findNode('node_modules');
285+
assert.ok(nodeModulesNode, 'node_modules should be in the tree');
286+
assert.strictEqual(nodeModulesNode!.checked, false, 'node_modules should be unchecked');
287+
288+
const errorLogNode = findNode('error.log');
289+
assert.ok(errorLogNode, 'error.log should be in the tree');
290+
assert.strictEqual(errorLogNode!.checked, false, 'error.log should be unchecked');
291+
292+
const distNode = findNode('dist');
293+
assert.ok(distNode, 'dist should be in the tree');
294+
assert.strictEqual(distNode!.checked, false, 'dist should be unchecked');
295+
296+
const buildNode = findNode('build');
297+
assert.ok(buildNode, 'build should be in the tree');
298+
assert.strictEqual(buildNode!.checked, false, 'build should be unchecked');
299+
300+
// Files/directories that should be included and checked
301+
const importantLogNode = findNode('important.log');
302+
assert.ok(importantLogNode, 'important.log should be in the tree');
303+
assert.strictEqual(importantLogNode!.checked, true, 'important.log should be checked');
304+
305+
const fileTxtNode = findNode('file.txt');
306+
assert.ok(fileTxtNode, 'file.txt should be in the tree');
307+
assert.strictEqual(fileTxtNode!.checked, true, 'file.txt should be checked');
308+
309+
const srcNode = findNode('src');
310+
assert.ok(srcNode, 'src should be in the tree');
311+
assert.strictEqual(srcNode!.checked, true, 'src should be checked');
312+
313+
// Check subdirectory with its own gitignore
314+
const subdirNode = findNode('subdir');
315+
assert.ok(subdirNode, 'subdir should be in the tree');
316+
assert.strictEqual(subdirNode!.checked, true, 'subdir should be checked');
317+
318+
// Find nodes in the subdirectory
319+
const subdirChildren = subdirNode!.children!;
320+
const findSubdirNode = (name: string) => {
321+
return subdirChildren.find(node => node.name === name);
322+
};
323+
324+
const subdirFileTxtNode = findSubdirNode('file.txt');
325+
assert.ok(subdirFileTxtNode, 'subdir/file.txt should be in the tree');
326+
assert.strictEqual(subdirFileTxtNode!.checked, false, 'subdir/file.txt should be unchecked');
327+
328+
const subdirImportantTxtNode = findSubdirNode('important.txt');
329+
assert.ok(subdirImportantTxtNode, 'subdir/important.txt should be in the tree');
330+
assert.strictEqual(subdirImportantTxtNode!.checked, true, 'subdir/important.txt should be checked');
331+
332+
const subdirCodeJsNode = findSubdirNode('code.js');
333+
assert.ok(subdirCodeJsNode, 'subdir/code.js should be in the tree');
334+
assert.strictEqual(subdirCodeJsNode!.checked, true, 'subdir/code.js should be checked');
335+
});
336+
});
337+
});

0 commit comments

Comments
 (0)