Files
scriptc/packages/runtime/test/gen-glob-cases.mjs
T
2026-10-14 03:54:52 -05:00

470 lines
7.7 KiB
JavaScript

// Generates path.matchesGlob test cases with Node as the oracle.
//
// node --expose-internals gen-glob-cases.mjs > glob-cases.txt
//
// Lines are tab-separated:
// p <path> adds a path to the curated list
// c <pattern> <posix> <win32> a pattern against every curated path
// f <path> <pattern> <posix> <win32> one fuzz case
// Strings keep printable ASCII and spell everything else (and "\") as
// \uXXXX UTF-16 code units, so lone surrogates survive. Each answer is two
// characters per path, case-sensitive then case-insensitive ("1", "0", or
// "E" when Node throws): Node 24 matches case-insensitively on macOS and
// Windows hosts, so both modes are recorded through the bundled minimatch
// with matchGlobPattern's exact options. Node v24 is required: the cases pin
// its bundled minimatch.
import { posix, win32 } from "node:path";
import { stdout } from "node:process";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { minimatch } = require("internal/deps/minimatch/index");
// The pattern has no u flag, so each match is one UTF-16 code unit.
const escape = (s) =>
s.replace(/[^\x20-\x5b\x5d-\x7e]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
const options = (windows, nocase) => ({
nocase,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: windows ? "win32" : "posix",
nocaseMagicOnly: true,
});
const lines = [];
const answer = (path, pattern, windows, nocase) => {
try {
return minimatch(path, pattern, options(windows, nocase)) ? "1" : "0";
} catch {
return "E";
}
};
const hostNocase = process.platform === "darwin" || process.platform === "win32";
// Both case modes for one family; the public API on this host must agree
// with the recorded options.
const answers = (path, pattern, windows) => {
const expected = [false, true].map((nocase) => answer(path, pattern, windows, nocase));
let actual;
try {
actual = (windows ? win32 : posix).matchesGlob(path, pattern) ? "1" : "0";
} catch {
actual = "E";
}
if (actual !== expected[hostNocase ? 1 : 0])
throw new Error(`oracle mismatch: ${path} ${pattern}`);
return expected.join("");
};
// ── curated vocabulary ──────────────────────────────────────────────
const paths = [
"",
".",
"..",
"/",
"a",
"A",
"ab",
"a.b",
"a.js",
"A.JS",
".a",
".js",
"a/b",
"a/b/c",
"a/b/c/d",
"a/.b",
"a/.b/c",
"a/../b",
"./a",
"../a",
"a//b",
"a/b/",
"/a",
"/a/b/c.js",
"//a/b",
"x/y/.z.ts",
"src/lib/index.test.ts",
"node_modules/pkg/index.js",
"a\\b",
"C:\\a\\b",
"c:\\a",
"C:/a/b",
"\\\\server\\share\\x",
"//server/share/x",
"//?/C:/x",
"a b",
"a,b",
"a{b}",
"a(b)",
"[a]",
"a!b",
"a+b",
"a*b",
"file.tar.gz",
"file.",
"1",
"12",
"a10",
"é",
"É",
"😀",
"a😀b",
"\ud83d",
"abc/def/ghi.txt",
"+(a)",
"\t",
"a\nb",
"aaa",
"b",
"ba",
"a/../../b",
"../../a",
];
const patterns = [
"",
"*",
"**",
"***",
"?",
"??",
"???",
"a",
"A",
"a*",
"*a",
"*a*",
"a?",
"?a",
".*",
"*.*",
"*.js",
"*.JS",
"**.js",
"**/*.js",
"**/*",
"*/*",
"*/**",
"**/**",
"a/**",
"a/**/b",
"a/**/*",
"**/a",
"a/*/c",
"a/*/*",
"/a",
"/**",
"/*",
"//a",
"a/b/",
"a/b",
"./a",
"../a",
"a/../b",
"a/./b",
"a/.b",
"a/.*",
".a",
".a/*",
"**/.b",
"**/.*",
"**/../a",
"a/**/../b",
"a/**/../b/c",
"{a,b}",
"{a,b}/c",
"{,a}",
"{a,}",
"{a}",
"{}",
"{}a",
"a{}",
"{a,b{c,d}}",
"{a..c}",
"{c..a}",
"{1..3}",
"{01..3}",
"{3..1}",
"{1..10..3}",
"{a..e..2}",
"{-2..2}",
"{a,b,c}{1,2}",
"x{a,b}y",
"{a/b,c}",
"${a,b}",
"{a,b}}",
"{{a,b}",
"{a,{b,c}}",
"{a\\,b}",
"{1..1}",
"{A..c}",
"{x..z,a}",
"[abc]",
"[a-c]",
"[!a]",
"[^a]",
"[a-]",
"[-a]",
"[]]",
"[]a]",
"[!]]",
"[a-z0-9]",
"[z-a]",
"[a",
"a]",
"[[]",
"[\\]]",
"[.]*",
"[[:alpha:]]",
"[[:digit:]]*",
"[[:upper:]]",
"[[:lower:]]",
"[[:alnum:]_]",
"[[:space:]]",
"[[:punct:]]",
"[[:xdigit:]]",
"[[:word:]]",
"[[:ascii:]]",
"[![:alpha:]]",
"[[:graph:]]",
"[[:print:]]",
"[[:cntrl:]]",
"[[:blank:]]",
"[a-[:digit:]]",
"[[:bogus:]]",
"[é]",
"[😀]",
"[à-ü]",
"+(a)",
"*(a)",
"?(a)",
"@(a)",
"!(a)",
"!(a|b)",
"+(a|b)",
"*(a|b)c",
"a+(b)",
"a*(b)",
"a?(b)",
"a@(b|c)",
"a!(b)",
"!(*.js)",
"!(.*)",
"*.!(js)",
"!(a)/b",
"a/!(b)",
"+(a|*.js)",
"@(a|b)/**",
"!(!(a))",
"+(+(a))",
"*(+(a))",
"?(*(a))",
"@(+(a))",
"+(?(a))",
"!(@(a))",
"@(!(a))",
"+(*(a)|b)",
"*(a|+(b))",
"!()",
"+()",
"@()",
"?()",
"*()",
"a!()",
"!(a",
"+(a",
"a)",
"(a)",
"+(a)b)",
"!(a)*",
"*!(a)",
"+(a|)",
"@(|a)",
"!(|a)",
"x/+(a|b)/y",
"+([a-c])",
"!([a-c])",
"@(*.ts|*.js)",
"**/@(a|b).js",
"?(a|b|c)?",
"!(foo).js",
"!(foo)*.js",
"+(!(a))",
"*(!(a))",
"!(+(a))",
"!(*(a))",
"!(?(a))",
"#a",
"!a",
"!!a",
"a\\b",
"\\a",
"a\\",
"C:/a/*",
"c:/a/*",
"C:\\a\\*",
"C:/**",
"//server/share/*",
"//?/C:/*",
"//?/c:/x",
"*/share/*",
"**/c",
"**/*.ts",
"src/**/*.ts",
"src/**/!(*.test).ts",
"**/node_modules/**",
"**/*.{js,ts}",
"**/*.+(js|ts)",
"docs/*.md",
"*.min.js",
"*.js.map",
"**/index.@(ts|js)",
"x/**/z.ts",
"x/*/z.ts",
"x/**/.z.ts",
"a/b/**/c/**/d",
"**/b/**",
"a/**/b/**/c",
"?/?",
"??/*",
"*.?",
"a.?",
"a??",
"a/?/c",
"é",
"É",
"日*",
"😀",
"?😀?",
"a?b",
"a*b*c",
"a**b",
"**a",
"a**",
"*/",
"**/",
"/",
"//",
".",
"..",
"./*",
"../*",
"*.tar.*",
"file.*",
"*.",
"[.]",
"\\*",
"a{b,c",
"a{b..c}",
"{a,b,c,d,e}{f,g}",
"*{.js,.ts}",
"+(a|b)+(c|d)",
"!(a).!(b)",
"@(a|b|c|d|e|f|g)",
"*(?)",
"+(*)",
"!(*)",
"@(*)",
"?(*)",
"\t",
"a\nb",
" ",
"*\n*",
"[\n]",
"a/../../b",
"../../*",
"**/../../a",
"a/b/../../../c",
];
for (const path of paths) lines.push(`p\t${escape(path)}`);
for (const pattern of patterns) {
const family = (windows) => paths.map((path) => answers(path, pattern, windows)).join("");
lines.push(`c\t${escape(pattern)}\t${family(false)}\t${family(true)}`);
}
// ── seeded fuzz over glob syntax ────────────────────────────────────
let seed = 20261014;
const random = () => {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
};
const pick = (list) => list[Math.floor(random() * list.length)];
const patternTokens = [
"a",
"b",
"c",
".",
"/",
"*",
"**",
"?",
"[",
"]",
"!",
"^",
"-",
"(",
")",
"|",
"+",
"@",
"{",
"}",
",",
"..",
"\\",
":",
"A",
"B",
"é",
"1",
"2",
"[:alpha:]",
"[:digit:]",
"!(",
"+(",
"*(",
"?(",
"@(",
"{a,b}",
"{1..3}",
"[a-c]",
"[!b]",
"C:",
];
const pathTokens = [
"a",
"b",
"c",
".",
"..",
"/",
"\\",
"A",
"B",
"ab",
"é",
"1",
"2",
"-",
"C:",
"+",
"(",
")",
];
const make = (tokens, max) => {
const n = 1 + Math.floor(random() * max);
let s = "";
for (let i = 0; i < n; i++) s += pick(tokens);
return s;
};
// A zero step never ends: Node's expansion fails with RangeError (slowly).
const fuzz = [["0", "{0..1..0}"]];
for (let i = 0; i < 3000; i++) fuzz.push([make(pathTokens, 6), make(patternTokens, 7)]);
for (const [path, pattern] of fuzz) {
const results = [false, true].map((windows) => answers(path, pattern, windows));
lines.push(`f\t${escape(path)}\t${escape(pattern)}\t${results.join("\t")}`);
}
stdout.write(lines.join("\n") + "\n");