Files
node/test/parallel/test-fs-glob-exclude-function-async.mjs
avivkeller a8f2bf007e fs: implement glob natively
Signed-off-by: avivkeller <me@aviv.sh>
PR-URL: https://github.com/nodejs/node/pull/65392
Refs: https://openjs-foundation.slack.com/archives/C019Y2T6STH/p1787069174254509
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-09-11 13:15:27 -04:00

145 lines
4.9 KiB
JavaScript

// An `exclude` callback can only run on the main thread. The async APIs
// still must not hold the event loop for the whole walk: the walk
// advances in short main-thread slices between which the loop turns.
import * as common from '../common/index.mjs';
import tmpdir from '../common/tmpdir.js';
import assert from 'node:assert';
import { glob, globSync } from 'node:fs';
import { glob as globIterator, mkdir, writeFile } from 'node:fs/promises';
import { dirname, join, sep } from 'node:path';
import { promisify } from 'node:util';
tmpdir.refresh();
const cwd = tmpdir.resolve('tree');
const files = ['a/b/c/d.txt', 'a/b/e.txt', 'a/f/g.txt', 'h/i.txt', 'j.txt'];
for (const file of files) {
await mkdir(join(cwd, dirname(file)), { recursive: true });
await writeFile(join(cwd, file), '');
}
const expected = files.map((file) => file.replaceAll('/', sep)).sort();
assert.deepStrictEqual(globSync('**/*.txt', { cwd }).sort(), expected);
// Long enough that the slice it runs in overruns its time budget, so the
// next directory is visited from a later loop turn.
function stall(ms) {
const end = performance.now() + ms;
while (performance.now() < end);
}
// Counts loop turns for as long as the walks run.
let turn = 0;
let walking = true;
(function tick() {
turn++;
if (walking) setImmediate(tick);
})();
// Records the loop turns its calls landed in.
function makeExclude() {
const turns = new Set();
const exclude = common.mustCallAtLeast((name) => {
assert.strictEqual(typeof name, 'string');
turns.add(turn);
stall(2);
return name === 'f';
}, 1);
return { exclude, turns };
}
const expectedExcluded = expected.filter((file) => !file.includes(`${sep}f${sep}`));
{
// Callback API
const { exclude, turns } = makeExclude();
await new Promise((resolve) => {
glob('**/*.txt', { cwd, exclude }, common.mustSucceed((results) => {
assert.deepStrictEqual(results.sort(), expectedExcluded);
resolve();
}));
// Nothing of the walk runs before the call returns.
assert.strictEqual(turns.size, 0);
});
assert.ok(turns.size >= 2, `exclude ran within ${turns.size} loop turn(s)`);
}
{
// Promisified callback API
const { exclude, turns } = makeExclude();
const pending = promisify(glob)('**/*.txt', { cwd, exclude });
assert.strictEqual(turns.size, 0);
assert.deepStrictEqual((await pending).sort(), expectedExcluded);
assert.ok(turns.size >= 2, `exclude ran within ${turns.size} loop turn(s)`);
}
{
// Async iterator
const { exclude, turns } = makeExclude();
const results = [];
for await (const entry of globIterator('**/*.txt', { cwd, exclude })) {
results.push(entry);
}
assert.deepStrictEqual(results.sort(), expectedExcluded);
assert.ok(turns.size >= 2, `exclude ran within ${turns.size} loop turn(s)`);
}
{
// Leaving the iterator early cancels a walk that is not over: the
// directory visited first (the last in listing order) holds more
// entries than one batch, so it fills the first batch while the other
// directories are still queued.
const many = tmpdir.resolve('many');
for (const dir of ['a', 'b', 'zz']) {
await mkdir(join(many, dir), { recursive: true });
await writeFile(join(many, dir, 'file.txt'), '');
}
for (let i = 0; i < 300; i++) {
await writeFile(join(many, 'zz', `${i}.txt`), '');
}
let calls = 0;
const exclude = () => {
calls++;
return false;
};
let seen = 0;
for await (const entry of globIterator('**/*.txt', { cwd: many, exclude })) {
assert.ok(entry.endsWith('.txt'), entry);
if (++seen === 2) break;
}
assert.strictEqual(seen, 2);
const callsAtBreak = calls;
await new Promise((resolve) => setTimeout(resolve, 20));
assert.strictEqual(calls, callsAtBreak);
}
{
// A callback that throws fails the walk with its error.
const error = new Error('exclude failed');
const exclude = common.mustCall(() => { throw error; });
glob('**/*.txt', { cwd, exclude }, common.mustCall((err, results) => {
assert.strictEqual(err, error);
assert.strictEqual(results, undefined);
}));
await assert.rejects(
promisify(glob)('**/*.txt', { cwd, exclude: common.mustCall(() => { throw error; }) }),
(err) => err === error);
await assert.rejects(
(async () => {
for await (const entry of globIterator('**/*.txt', { cwd, exclude: common.mustCall(() => { throw error; }) })) {
assert.fail(`unexpected result ${entry}`);
}
})(),
(err) => err === error);
}
{
// withFileTypes: the callback sees Dirents.
const { exclude: base, turns } = makeExclude();
const exclude = (dirent) => base(dirent.name);
const results = await promisify(glob)('**/*.txt', { cwd, exclude, withFileTypes: true });
assert.deepStrictEqual(
results.map((dirent) => join(dirent.parentPath, dirent.name).slice(cwd.length + 1)).sort(),
expectedExcluded);
assert.ok(turns.size >= 2, `exclude ran within ${turns.size} loop turn(s)`);
}
walking = false;