Upstream mrmlnc/fast-glob state of the touched files before 0754373f47

This commit is contained in:
upstream committed 2026-09-03 14:37:27 +02:00
commit ed3aca1ad6
3 files changed
+528

No files matched your search

+357
View File
@@ -0,0 +1,357 @@
import * as assert from 'node:assert';
import { describe, it } from 'mocha';
import * as tests from './tests/index.js';
import type { EntryItem, ErrnoException } from './types/index.js';
import * as fg from './index.js';
// Only for validating the input data.
const invalidInputData = null as unknown as string;
describe('Package', () => {
describe('.globSync', () => {
it('should throw an error when input values can not pass validation', () => {
const message = 'Patterns must be a string (non empty) or an array of strings';
assert.throws(() => fg.globSync(invalidInputData), { message });
assert.throws(() => fg.globSync(''), { message });
});
it('should returns entries', () => {
const expected: EntryItem[] = [
'fixtures/file.md',
'fixtures/first/file.md',
'fixtures/first/nested/directory/file.md',
'fixtures/first/nested/file.md',
'fixtures/second/file.md',
'fixtures/second/nested/directory/file.md',
'fixtures/second/nested/file.md',
'fixtures/third/library/a/book.md',
'fixtures/third/library/b/book.md',
];
const actual = fg.globSync(['fixtures/**/*.md']);
actual.sort((a, b) => a.localeCompare(b));
assert.deepStrictEqual(actual, expected);
});
it('should returns entries (two sources)', () => {
const expected: EntryItem[] = [
'fixtures/first/file.md',
'fixtures/first/nested/directory/file.md',
'fixtures/first/nested/file.md',
'fixtures/second/file.md',
'fixtures/second/nested/directory/file.md',
'fixtures/second/nested/file.md',
];
const actual = fg.globSync(['fixtures/first/**/*.md', 'fixtures/second/**/*.md']);
actual.sort((a, b) => a.localeCompare(b));
assert.deepStrictEqual(actual, expected);
});
});
describe('.sync', () => {
it('should be an alias for the .globSync method', () => {
// eslint-disable-next-line @typescript-eslint/no-deprecated
assert.strictEqual(fg.sync, fg.globSync);
});
});
describe('.glob', () => {
it('should throw an error when input values can not pass validation', async () => {
const message = 'Patterns must be a string (non empty) or an array of strings';
await assert.rejects(async () => fg.glob(invalidInputData), { message });
await assert.rejects(async () => fg.glob(''), { message });
});
it('should returns entries', async () => {
const expected: EntryItem[] = [
'fixtures/file.md',
'fixtures/first/file.md',
'fixtures/first/nested/directory/file.md',
'fixtures/first/nested/file.md',
'fixtures/second/file.md',
'fixtures/second/nested/directory/file.md',
'fixtures/second/nested/file.md',
'fixtures/third/library/a/book.md',
'fixtures/third/library/b/book.md',
];
const actual = await fg.glob(['fixtures/**/*.md']);
actual.sort((a, b) => a.localeCompare(b));
assert.deepStrictEqual(actual, expected);
});
it('should returns entries (two sources)', async () => {
const expected: EntryItem[] = [
'fixtures/first/file.md',
'fixtures/first/nested/directory/file.md',
'fixtures/first/nested/file.md',
'fixtures/second/file.md',
'fixtures/second/nested/directory/file.md',
'fixtures/second/nested/file.md',
];
const actual = await fg.glob(['fixtures/first/**/*.md', 'fixtures/second/**/*.md']);
actual.sort((a, b) => a.localeCompare(b));
assert.deepStrictEqual(actual, expected);
});
it('should abort processing dynamic pattern with abort signal', async () => {
const signal = AbortSignal.timeout(5);
// The globstar pattern is used here to make the call run longer than the settimeout.
const action = fg.glob(['**'], { signal });
await assert.rejects(async () => action, { message: 'The operation was aborted due to timeout' });
});
it('should abort processing static pattern with abort signal', async () => {
const ac = new AbortController();
ac.abort();
const action = fg.glob(['./package.json'], { signal: ac.signal });
await assert.rejects(async () => action, { message: 'The operation was aborted' });
});
});
describe('.async', () => {
it('should be an alias for the .glob method', () => {
// eslint-disable-next-line @typescript-eslint/no-deprecated
assert.strictEqual(fg.async, fg.glob);
});
});
describe('.globStream', () => {
it('should throw an error when input values can not pass validation', () => {
const message = 'Patterns must be a string (non empty) or an array of strings';
assert.throws(() => fg.globStream(invalidInputData), { message });
assert.throws(() => fg.globStream(''), { message });
});
it('should returns entries', (done) => {
const expected: string[] = [
'fixtures/file.md',
'fixtures/first/file.md',
'fixtures/first/nested/directory/file.md',
'fixtures/first/nested/file.md',
'fixtures/second/file.md',
'fixtures/second/nested/directory/file.md',
'fixtures/second/nested/file.md',
'fixtures/third/library/a/book.md',
'fixtures/third/library/b/book.md',
];
const actual: string[] = [];
const stream = fg.globStream(['fixtures/**/*.md']);
stream.on('data', (entry: string) => {
actual.push(entry);
});
stream.once('error', (error: ErrnoException) => assert.fail(error));
stream.once('end', () => {
actual.sort();
assert.deepStrictEqual(actual, expected);
done();
});
});
it('should returns entries (two sources)', (done) => {
const expected: EntryItem[] = [
'fixtures/first/file.md',
'fixtures/first/nested/directory/file.md',
'fixtures/first/nested/file.md',
'fixtures/second/file.md',
'fixtures/second/nested/directory/file.md',
'fixtures/second/nested/file.md',
];
const actual: string[] = [];
const stream = fg.globStream(['fixtures/first/**/*.md', 'fixtures/second/**/*.md']);
stream.on('data', (entry: string) => {
actual.push(entry);
});
stream.once('error', (error: ErrnoException) => assert.fail(error));
stream.once('end', () => {
actual.sort();
assert.deepStrictEqual(actual, expected);
done();
});
});
it('should abort processing dynamic pattern with abort signal', (done) => {
const signal = AbortSignal.timeout(5);
// The globstar pattern is used here to make the call run longer than the settimeout.
const steam = fg.globStream(['**'], { signal });
steam.once('error', (error: ErrnoException) => {
assert.strictEqual(error.message, 'The operation was aborted due to timeout');
done();
});
steam.once('end', () => {
assert.fail('The stream should be aborted');
});
});
it('should abort processing static pattern with abort signal', (done) => {
const ac = new AbortController();
ac.abort();
// The globstar pattern is used here to make the call run longer than the settimeout.
const steam = fg.globStream(['./package.json'], { signal: ac.signal });
steam.once('error', (error: ErrnoException) => {
assert.strictEqual(error.message, 'The operation was aborted');
done();
});
steam.once('end', () => {
assert.fail('The stream should be aborted');
});
});
});
describe('.stream', () => {
it('should be an alias for the .globStream method', () => {
// eslint-disable-next-line @typescript-eslint/no-deprecated
assert.strictEqual(fg.stream, fg.globStream);
});
});
describe('.generateTasks', () => {
it('should throw an error when input values can not pass validation', () => {
const message = 'Patterns must be a string (non empty) or an array of strings';
assert.throws(() => fg.generateTasks(invalidInputData), { message });
assert.throws(() => fg.generateTasks(''), { message });
});
it('should return tasks', () => {
const expected = [
tests.task.builder().base('.').positive('*').build(),
];
const actual = fg.generateTasks(['*']);
assert.deepStrictEqual(actual, expected);
});
it('should return tasks with negative patterns', () => {
const expected = [
tests.task.builder().base('.').positive('*').negative('*.txt').negative('*.md').build(),
];
const actual = fg.generateTasks(['*', '!*.txt'], { ignore: ['*.md'] });
assert.deepStrictEqual(actual, expected);
});
it('should clean up patterns', () => {
const expected = [
// Clean up duplicate slashes
tests.task.builder().base('fixtures').positive('fixtures/*').build(),
];
const actual = fg.generateTasks(['fixtures//*']);
assert.deepStrictEqual(actual, expected);
});
});
describe('.isDynamicPattern', () => {
it('should return true for dynamic pattern', () => {
assert.ok(fg.isDynamicPattern('*'));
});
it('should return false for static pattern', () => {
assert.ok(!fg.isDynamicPattern('abc'));
});
});
describe('.escapePath', () => {
it('should return escaped path', () => {
const expected = String.raw`C:/Program Files \(x86\)`;
const actual = fg.escapePath('C:/Program Files (x86)');
assert.strictEqual(actual, expected);
});
});
describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
// In posix system \\ is a escaping character and it will be escaped before non-special characters.
const posix = String.raw`C:\\Program Files \(x86\)\*\*\*`;
const windows = String.raw`C:/Program Files \(x86\)/**/*`;
const expected = tests.platform.isWindows() ? windows : posix;
const actual = fg.convertPathToPattern(String.raw`C:\Program Files (x86)\**\*`);
assert.strictEqual(actual, expected);
});
});
describe('posix', () => {
describe('.escapePath', () => {
it('should return escaped path', () => {
const expected = String.raw`/directory/\*\*/\*`;
const actual = fg.posix.escapePath(String.raw`/directory/*\*/*`);
assert.strictEqual(actual, expected);
});
});
describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
const expected = String.raw`a\*.txt`;
const actual = fg.posix.convertPathToPattern(String.raw`a\*.txt`);
assert.strictEqual(actual, expected);
});
});
});
describe('win32', () => {
describe('.escapePath', () => {
it('should return escaped path', () => {
const expected = String.raw`C:\Program Files \(x86\)\**\*`;
const actual = fg.win32.escapePath(String.raw`C:\Program Files (x86)\**\*`);
assert.strictEqual(actual, expected);
});
});
describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
const expected = String.raw`C:/Program Files \(x86\)/**/*`;
const actual = fg.win32.convertPathToPattern(String.raw`C:\Program Files (x86)\**\*`);
assert.strictEqual(actual, expected);
});
});
});
});
+115
View File
@@ -0,0 +1,115 @@
import * as assert from 'node:assert';
import * as path from 'node:path';
import * as process from 'node:process';
import { Stats, StatsMode } from '@nodelib/fs.macchiato';
import { describe, it } from 'mocha';
import Settings, { type Options } from '../settings.js';
import * as tests from '../tests/index.js';
import type {
Entry,
ErrnoException,
FsStats,
Pattern,
} from '../types/index.js';
import { Reader } from './reader.js';
class TestReader extends Reader<never[]> {
constructor(options?: Options) {
super(new Settings(options));
}
public dynamic(): never[] {
return [];
}
public static(): never[] {
return [];
}
public getFullEntryPath(filepath: string): string {
return this._getFullEntryPath(filepath);
}
public makeEntry(stats: FsStats, pattern: Pattern): Entry {
return this._makeEntry(stats, pattern);
}
public isFatalError(error: ErrnoException): boolean {
return this._isFatalError(error);
}
}
function getReader(options?: Options): TestReader {
return new TestReader(options);
}
describe('Readers → Reader', () => {
describe('Constructor', () => {
it('should create instance of class', () => {
const reader = getReader();
assert.ok(reader instanceof TestReader);
});
});
describe('.getFullEntryPath', () => {
it('should return path to entry', () => {
const reader = getReader();
const expected = path.join(process.cwd(), 'config.json');
const actual = reader.getFullEntryPath('config.json');
assert.strictEqual(actual, expected);
});
});
describe('.makeEntry', () => {
it('should return created entry', () => {
const reader = getReader();
const pattern = 'config.json';
const stats = new Stats({ mode: StatsMode.File });
const actual = reader.makeEntry(stats, pattern);
assert.strictEqual(actual.name, pattern);
assert.strictEqual(actual.path, pattern);
assert.ok(actual.dirent.isFile());
});
it('should return created entry with fs.Stats', () => {
const reader = getReader({ stats: true });
const pattern = 'config.json';
const actual = reader.makeEntry(new Stats(), pattern);
assert.ok(actual.stats);
});
});
describe('.isFatalError', () => {
it('should return false for ENOENT error', () => {
const reader = getReader();
assert.ok(!reader.isFatalError(tests.errno.getEnoent()));
});
it('should return true for EPERM error', () => {
const reader = getReader();
assert.ok(reader.isFatalError(tests.errno.getEperm()));
});
it('should return true for ENOENT error when the `errorFilter` option returns false', () => {
const reader = getReader({ errorFilter: () => false });
assert.ok(reader.isFatalError(tests.errno.getEnoent()));
});
it('should return false for EPERM error when the `errorFilter` option returns true', () => {
const reader = getReader({ errorFilter: () => true });
assert.ok(!reader.isFatalError(tests.errno.getEperm()));
});
});
});
+56
View File
@@ -0,0 +1,56 @@
import * as path from 'node:path';
import * as fsStat from '@nodelib/fs.stat';
import * as utils from '../utils/index.js';
import type Settings from '../settings.js';
import type {
Entry,
ErrnoException,
FsStats,
Pattern,
ReaderOptions,
} from '../types/index.js';
export abstract class Reader<T> {
readonly #settings: Settings;
protected readonly _fsStatSettings: fsStat.Settings;
constructor(settings: Settings) {
this.#settings = settings;
this._fsStatSettings = new fsStat.Settings({
followSymbolicLink: settings.followSymbolicLinks,
fs: settings.fs,
throwErrorOnBrokenSymbolicLink: settings.throwErrorOnBrokenSymbolicLink,
});
}
public abstract dynamic(root: string, options: ReaderOptions): T;
public abstract static(patterns: Pattern[], options: ReaderOptions): T;
protected _getFullEntryPath(filepath: string): string {
return path.resolve(this.#settings.cwd, filepath);
}
protected _makeEntry(stats: FsStats, pattern: Pattern): Entry {
const entry: Entry = {
name: pattern,
path: pattern,
dirent: utils.fs.createDirentFromStats(pattern, stats),
};
if (this.#settings.stats) {
entry.stats = stats;
}
return entry;
}
protected _isFatalError(error: ErrnoException): boolean {
if (this.#settings.errorFilter !== undefined) {
return !this.#settings.errorFilter(error);
}
return !utils.errno.isEnoentCodeError(error) && !utils.errno.isEnotdirCodeError(error);
}
}