Skip to content
File

Blob: src/workerd/api/node/tests/fs-glob-test.js

javascript358 lines
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import { deepStrictEqual, ok, strictEqual } from 'node:assert';
5import { glob, globSync, mkdirSync, writeFileSync, promises } from 'node:fs';
6 
7// Node.js test fixture structure (minus symlinks — not supported in workerd VFS tests)
8function setupFixtures() {
9 const base = '/tmp/gt';
10 mkdirSync(base + '/a/.abcdef/x/y/z', { recursive: true });
11 mkdirSync(base + '/a/abcdef/g', { recursive: true });
12 mkdirSync(base + '/a/abcfed/g', { recursive: true });
13 mkdirSync(base + '/a/b/c', { recursive: true });
14 mkdirSync(base + '/a/bc/e', { recursive: true });
15 mkdirSync(base + '/a/c/d/c', { recursive: true });
16 mkdirSync(base + '/a/cb/e', { recursive: true });
17 mkdirSync(base + '/a/x/.y', { recursive: true });
18 mkdirSync(base + '/a/z/.y', { recursive: true });
19 
20 writeFileSync(base + '/a/.abcdef/x/y/z/a', 'x');
21 writeFileSync(base + '/a/abcdef/g/h', 'x');
22 writeFileSync(base + '/a/abcfed/g/h', 'x');
23 writeFileSync(base + '/a/b/c/d', 'x');
24 writeFileSync(base + '/a/bc/e/f', 'x');
25 writeFileSync(base + '/a/c/d/c/b', 'x');
26 writeFileSync(base + '/a/cb/e/f', 'x');
27 writeFileSync(base + '/a/x/.y/b', 'x');
28 writeFileSync(base + '/a/z/.y/b', 'x');
29 writeFileSync(base + '/a/.b', 'x');
30 writeFileSync(base + '/a/b/.b', 'x');
31 
32 return base;
33}
34 
35const cwd = '/tmp/gt';
36 
37// ============================================================================
38// Pattern Matching Tests (ported from Node.js)
39// ============================================================================
40 
41export const singleWildcardInSubdir = {
42 test() {
43 setupFixtures();
44 const result = globSync('a/c/d/*/b', { cwd });
45 deepStrictEqual(result.sort(), ['a/c/d/c/b']);
46 },
47};
48 
49export const doubleSlashNormalization = {
50 test() {
51 setupFixtures();
52 // Double slashes should be normalized
53 const result = globSync('a//c//d//*//b', { cwd });
54 deepStrictEqual(result.sort(), ['a/c/d/c/b']);
55 },
56};
57 
58export const multipleWildcards = {
59 test() {
60 setupFixtures();
61 const result = globSync('a/*/d/*/b', { cwd });
62 deepStrictEqual(result.sort(), ['a/c/d/c/b']);
63 },
64};
65 
66export const extglobPlus = {
67 test() {
68 setupFixtures();
69 // +(c|g) should match one or more occurrences of c or g
70 // a/*/+(c|g)/./d → matches a/b/c/./d which resolves to a/b/c/d
71 const result = globSync('a/*/+(c|g)/./d', { cwd });
72 deepStrictEqual(result.sort(), ['a/b/c/d']);
73 },
74};
75 
76export const braceExpansion = {
77 test() {
78 setupFixtures();
79 const result = globSync('a/abc{fed,def}/g/h', { cwd });
80 deepStrictEqual(result.sort(), ['a/abcdef/g/h', 'a/abcfed/g/h']);
81 },
82};
83 
84export const braceExpansionNoMatch = {
85 test() {
86 setupFixtures();
87 // None of b,c,d,e,f directories have a **/g path
88 const result = globSync('a/{b,c,d,e,f}/**/g', { cwd });
89 deepStrictEqual(result, []);
90 },
91};
92 
93export const globstarMatchesAll = {
94 test() {
95 setupFixtures();
96 // a/b/** should match a/b itself and everything under it
97 const result = globSync('a/b/**', { cwd });
98 deepStrictEqual(result.sort(), ['a/b', 'a/b/.b', 'a/b/c', 'a/b/c/d']);
99 },
100};
101 
102export const dotSlashGlobstar = {
103 test() {
104 setupFixtures();
105 // ./**/g should find all 'g' entries
106 const result = globSync('./**/g', { cwd });
107 deepStrictEqual(result.sort(), ['a/abcdef/g', 'a/abcfed/g']);
108 },
109};
110 
111export const globstarA = {
112 test() {
113 setupFixtures();
114 // **/a — find all entries named 'a'
115 const result = globSync('**/a', { cwd });
116 ok(result.includes('a'));
117 ok(result.includes('a/.abcdef/x/y/z/a'));
118 },
119};
120 
121export const threeWildcardLevelsF = {
122 test() {
123 setupFixtures();
124 const result = globSync('*/*/*/f', { cwd });
125 deepStrictEqual(result.sort(), ['a/bc/e/f', 'a/cb/e/f']);
126 },
127};
128 
129export const dotSlashGlobstarF = {
130 test() {
131 setupFixtures();
132 const result = globSync('./**/f', { cwd });
133 deepStrictEqual(result.sort(), ['a/bc/e/f', 'a/cb/e/f']);
134 },
135};
136 
137export const dotFileGlobstar = {
138 test() {
139 setupFixtures();
140 // **/.b should match dot files named .b
141 const result = globSync('**/.b', { cwd });
142 deepStrictEqual(result.sort(), ['a/.b', 'a/b/.b']);
143 },
144};
145 
146export const globstarCharClass = {
147 test() {
148 setupFixtures();
149 // a/**/[cg] — match entries named 'c' or 'g' at any depth under a/
150 const result = globSync('a/**/[cg]', { cwd });
151 ok(result.includes('a/abcdef/g'));
152 ok(result.includes('a/abcfed/g'));
153 ok(result.includes('a/b/c'));
154 ok(result.includes('a/c'));
155 ok(result.includes('a/c/d/c'));
156 },
157};
158 
159export const parentTraversal = {
160 test() {
161 setupFixtures();
162 // a/**/[cg]/../[cg] — go into [cg] dir, go up, match [cg] again
163 const result = globSync('a/**/[cg]/../[cg]', { cwd });
164 ok(result.length > 0);
165 // Should find paths where a directory matching [cg] has a sibling matching [cg]
166 ok(result.includes('a/c'));
167 },
168};
169 
170export const extglobNegation = {
171 test() {
172 setupFixtures();
173 // a/!(doesnotexist)/** — match all under a/ except entries named 'doesnotexist'
174 const result = globSync('a/!(doesnotexist)/**', { cwd });
175 ok(result.length > 0);
176 ok(result.includes('a/b'));
177 ok(result.includes('a/b/c'));
178 ok(result.includes('a/b/c/d'));
179 },
180};
181 
182// ============================================================================
183// Exclude Tests
184// ============================================================================
185 
186export const excludeFunction = {
187 test() {
188 setupFixtures();
189 // Exclude everything — should return empty
190 const result = globSync('a/**', { cwd, exclude: () => true });
191 deepStrictEqual(result, []);
192 },
193};
194 
195export const excludeFunctionSelective = {
196 test() {
197 setupFixtures();
198 // Exclude paths containing 'abcdef'
199 const result = globSync('a/abc*/g/h', {
200 cwd,
201 exclude: (p) => p.includes('abcdef'),
202 });
203 deepStrictEqual(result, ['a/abcfed/g/h']);
204 },
205};
206 
207export const excludeArrayPattern = {
208 test() {
209 setupFixtures();
210 // Exclude using array of glob patterns
211 const result = globSync('a/abc*/g/h', {
212 cwd,
213 exclude: ['**/abcdef/**'],
214 });
215 deepStrictEqual(result, ['a/abcfed/g/h']);
216 },
217};
218 
219export const excludeArrayMatchesAll = {
220 test() {
221 setupFixtures();
222 // Exclude all with wildcard
223 const result = globSync('a/**', { cwd, exclude: ['**'] });
224 deepStrictEqual(result, []);
225 },
226};
227 
228// ============================================================================
229// withFileTypes Tests
230// ============================================================================
231 
232export const withFileTypesTest = {
233 test() {
234 setupFixtures();
235 const result = globSync('a/b/c/d', { cwd, withFileTypes: true });
236 strictEqual(result.length, 1);
237 const dirent = result[0];
238 strictEqual(dirent.name, 'd');
239 ok(dirent.isFile());
240 ok(!dirent.isDirectory());
241 },
242};
243 
244export const withFileTypesDirTest = {
245 test() {
246 setupFixtures();
247 const result = globSync('a/b/c', { cwd, withFileTypes: true });
248 strictEqual(result.length, 1);
249 const dirent = result[0];
250 strictEqual(dirent.name, 'c');
251 ok(dirent.isDirectory());
252 ok(!dirent.isFile());
253 },
254};
255 
256// ============================================================================
257// Callback API Tests
258// ============================================================================
259 
260export const callbackGlobTest = {
261 async test() {
262 setupFixtures();
263 const result = await new Promise((resolve, reject) => {
264 glob('a/abc{fed,def}/g/h', { cwd }, (err, matches) => {
265 if (err) reject(err);
266 else resolve(matches);
267 });
268 });
269 deepStrictEqual([...result].sort(), ['a/abcdef/g/h', 'a/abcfed/g/h']);
270 },
271};
272 
273export const callbackGlobNoOptions = {
274 async test() {
275 const result = await new Promise((resolve, reject) => {
276 glob('*.nonexistent', (err, matches) => {
277 if (err) reject(err);
278 else resolve(matches);
279 });
280 });
281 deepStrictEqual(result, []);
282 },
283};
284 
285// ============================================================================
286// Promises API Tests
287// ============================================================================
288 
289export const promisesGlobTest = {
290 async test() {
291 setupFixtures();
292 const results = [];
293 for await (const entry of promises.glob('a/abc{fed,def}/g/h', { cwd })) {
294 results.push(entry);
295 }
296 deepStrictEqual(results.sort(), ['a/abcdef/g/h', 'a/abcfed/g/h']);
297 },
298};
299 
300export const promisesGlobstarTest = {
301 async test() {
302 setupFixtures();
303 const results = [];
304 for await (const entry of promises.glob('**/.b', { cwd })) {
305 results.push(entry);
306 }
307 deepStrictEqual(results.sort(), ['a/.b', 'a/b/.b']);
308 },
309};
310 
311// ============================================================================
312// Edge Cases
313// ============================================================================
314 
315export const noMatchTest = {
316 test() {
317 setupFixtures();
318 const result = globSync('nonexistent/**', { cwd });
319 deepStrictEqual(result, []);
320 },
321};
322 
323export const exactPathTest = {
324 test() {
325 setupFixtures();
326 // Exact path match (no wildcards)
327 const result = globSync('a/b/c/d', { cwd });
328 deepStrictEqual(result, ['a/b/c/d']);
329 },
330};
331 
332export const defaultCwdTest = {
333 test() {
334 // Without cwd option, should use process.cwd() and not throw
335 const result = globSync('*.nonexistent');
336 deepStrictEqual(result, []);
337 },
338};
339 
340export const multiplePatternsTest = {
341 test() {
342 setupFixtures();
343 const result = globSync(['a/b/c/d', 'a/bc/e/f'], { cwd });
344 deepStrictEqual(result.sort(), ['a/b/c/d', 'a/bc/e/f']);
345 },
346};
347 
348export const braceWithGlobstar = {
349 test() {
350 setupFixtures();
351 // a/{b/**,b/c} should match a/b and everything under it, plus a/b/c
352 const result = globSync('a/{b/**,b/c}', { cwd });
353 ok(result.includes('a/b'));
354 ok(result.includes('a/b/c'));
355 ok(result.includes('a/b/c/d'));
356 },
357};