|
1 | 1 | import * as common from '../common/index.mjs'; |
2 | 2 | import tmpdir from '../common/tmpdir.js'; |
| 3 | +import { spawnSync } from 'node:child_process'; |
3 | 4 | import { resolve, dirname, sep, relative, join, isAbsolute } from 'node:path'; |
4 | 5 | import { mkdir, writeFile, symlink, glob as asyncGlob } from 'node:fs/promises'; |
5 | 6 | import { glob, globSync, Dirent, chmodSync, writeFileSync, rmSync } from 'node:fs'; |
@@ -669,3 +670,78 @@ describe('globSync - ENOTDIR', function() { |
669 | 670 | } |
670 | 671 | }); |
671 | 672 | }); |
| 673 | + |
| 674 | +describe('glob - seen cache', function() { |
| 675 | + // Refs: https://github.com/nodejs/node/issues/62897 |
| 676 | + test('does not skip siblings after a seen child path', () => { |
| 677 | + // The glob traversal used to return early from the children loop when a |
| 678 | + // child path had already been seen through a different pattern context, |
| 679 | + // silently dropping the remaining siblings. Whether the bug triggered |
| 680 | + // depended on directory iteration order, so the child process pins the |
| 681 | + // order by patching readdir before loading the glob implementation. |
| 682 | + const script = ` |
| 683 | + const assert = require('node:assert'); |
| 684 | + const fs = require('node:fs'); |
| 685 | + const fsPromises = require('node:fs/promises'); |
| 686 | + const path = require('node:path'); |
| 687 | +
|
| 688 | + const cwd = process.argv[1]; |
| 689 | + const a = path.join(cwd, 'a'); |
| 690 | + fs.mkdirSync(path.join(a, 'b', 'c', 'd'), { recursive: true }); |
| 691 | + fs.mkdirSync(path.join(a, 'c', 'd', 'c'), { recursive: true }); |
| 692 | + fs.writeFileSync(path.join(a, 'x'), ''); |
| 693 | + fs.writeFileSync(path.join(a, 'z'), ''); |
| 694 | +
|
| 695 | + const originalReaddirSync = fs.readdirSync; |
| 696 | + const originalReaddir = fsPromises.readdir; |
| 697 | +
|
| 698 | + const reorder = (target, entries) => { |
| 699 | + if (!Array.isArray(entries) || target !== a) return entries; |
| 700 | + const names = ['c', 'b', 'x', 'z']; |
| 701 | + return names.map((name) => entries.find((entry) => entry.name === name)) |
| 702 | + .filter(Boolean); |
| 703 | + }; |
| 704 | +
|
| 705 | + fs.readdirSync = function(target, options) { |
| 706 | + return reorder(target, originalReaddirSync.call(this, target, options)); |
| 707 | + }; |
| 708 | + fsPromises.readdir = async function(target, options) { |
| 709 | + return reorder(target, await originalReaddir.call(this, target, options)); |
| 710 | + }; |
| 711 | +
|
| 712 | + const { Glob } = require('internal/fs/glob'); |
| 713 | + const expected = ['a/b', 'a/c', 'a/x', 'a/z']; |
| 714 | + const normalize = (results) => |
| 715 | + results.map((item) => item.replaceAll(path.sep, '/')).sort(); |
| 716 | +
|
| 717 | + (async () => { |
| 718 | + const syncResults = normalize(new Glob('a/**/../*', { cwd }).globSync()); |
| 719 | + for (const item of expected) { |
| 720 | + assert.ok(syncResults.includes(item), |
| 721 | + \`missing \${item} from sync results: \${syncResults}\`); |
| 722 | + } |
| 723 | +
|
| 724 | + const asyncResults = []; |
| 725 | + for await (const item of new Glob('a/**/../*', { cwd }).glob()) { |
| 726 | + asyncResults.push(item); |
| 727 | + } |
| 728 | + const normalized = normalize(asyncResults); |
| 729 | + for (const item of expected) { |
| 730 | + assert.ok(normalized.includes(item), |
| 731 | + \`missing \${item} from async results: \${normalized}\`); |
| 732 | + } |
| 733 | + })().catch((err) => { |
| 734 | + console.error(err); |
| 735 | + process.exitCode = 1; |
| 736 | + }); |
| 737 | + `; |
| 738 | + |
| 739 | + const seenDir = tmpdir.resolve('glob-seen'); |
| 740 | + const child = spawnSync( |
| 741 | + process.execPath, |
| 742 | + ['--expose-internals', '-e', script, seenDir], |
| 743 | + { encoding: 'utf8' }, |
| 744 | + ); |
| 745 | + assert.strictEqual(child.status, 0, child.stderr || child.stdout); |
| 746 | + }); |
| 747 | +}); |
0 commit comments