Files
scriptc/packages/runtime/test/test_glob.c
T
Chris Tate 0b468e40a1 Add fs.globSync, fs.glob and fs/promises glob
The three glob entry points run a port of Node 24's
lib/internal/fs/glob.js walk in scr_glob.c: the same LIFO queue of
(path, patterns) items over the sorted directory listings, the same
lstat, stat, readdir, realpath and seen-pattern caches, the same result
set in insertion order, and the same exclusion checks, so matches come
out in Node's order. Patterns compile with the existing minimatch port.
glob_match is split into a compile step and a test step so the walk can
reuse each compiled pattern and test it segment by segment.
nocaseMagicOnly is now an option, because exclude patterns turn it off.

Patterns (a string or string array) and options (cwd as a path or
file: URL, exclude as patterns or a function, followSymlinks,
withFileTypes) are validated in Node's order with Node's errors,
including the internal assertion Node hits for a non-string cwd with
exclude patterns. withFileTypes results are Dirent rows: name,
parentPath and the type probes. parentPath follows Node's rule for each
form: the joined path for globSync, the resolved path for fs/promises
glob.

fs.glob checks the callback and validates synchronously, then delivers
string[] or the walk's error through the filesystem callback queue.
fs/promises glob lowers as the source of a for await loop. The walk runs
when the loop starts, and the matches found before an error bind before
the error is thrown, as Node's async iterator yields them first. The
older glob(3)-based fs.glob branch is removed.

Corpus 5448-5452 and a node-types fixture check the behavior against
Node.
2026-10-15 07:59:19 -05:00

172 lines
5.7 KiB
C

/* Oracle test for path.matchesGlob.
* Reads the case file given as argv[1] (line format in gen-glob-cases.mjs:
* curated paths, per-pattern answer rows over them, and fuzz cases) and
* runs every case for both path families with case-sensitive and
* case-insensitive matching, each answer "1", "0", or "E" for a thrown
* error.
*
* Exit 0 = all pass; prints each mismatch (capped) and exits 1 otherwise.
*/
#include "../src/scr_runtime.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
long scr_str_live_count(void); /* provided by scr_string.c */
/* fs.glob's callback form (scr_glob.c) schedules through the filesystem
* callback queue in scr_bytes_io.c, which this binary does not link; the
* oracle never calls it. */
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer) {
(void)callback;
(void)buffer;
scr_dyn_release(result);
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
/* An escaped case string (printable ASCII, \uXXXX code units) to a string
* in the runtime's WTF-8 storage. */
static ScrStr *decode(const char *text) {
size_t len = strlen(text);
uint16_t *units = malloc((len ? len : 1) * sizeof *units);
size_t n = 0;
for (size_t i = 0; i < len; i++) {
if (text[i] == '\\' && text[i + 1] == 'u') {
const char *h = text + i + 2;
units[n++] = (uint16_t)(hex_val(h[0]) << 12 | hex_val(h[1]) << 8 | hex_val(h[2]) << 4 | hex_val(h[3]));
i += 5;
} else {
units[n++] = (uint8_t)text[i];
}
}
char *out = malloc(3 * n + 1);
size_t w = 0;
bool lone = false;
for (size_t i = 0; i < n; i++) {
uint32_t cp = units[i];
if (cp >= 0xD800 && cp < 0xDC00 && i + 1 < n && units[i + 1] >= 0xDC00 && units[i + 1] < 0xE000) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (units[i + 1] - 0xDC00);
i++;
} else if (cp >= 0xD800 && cp < 0xE000) {
lone = true;
}
if (cp < 0x80) {
out[w++] = (char)cp;
} else if (cp < 0x800) {
out[w++] = (char)(0xC0 | (cp >> 6));
out[w++] = (char)(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
out[w++] = (char)(0xE0 | (cp >> 12));
out[w++] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[w++] = (char)(0x80 | (cp & 0x3F));
} else {
out[w++] = (char)(0xF0 | (cp >> 18));
out[w++] = (char)(0x80 | ((cp >> 12) & 0x3F));
out[w++] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[w++] = (char)(0x80 | (cp & 0x3F));
}
}
if (lone) scr_wtf8_note();
ScrStr *s = scr_str_new(out, w);
free(out);
free(units);
return s;
}
/* Splits a line at tabs in place; empty fields are kept. */
static int split(char *line, char **fields, int max) {
int count = 0;
fields[count++] = line;
for (char *c = line; *c; c++)
if (*c == '\t') {
*c = 0;
if (count == max) return -1;
fields[count++] = c + 1;
}
return count;
}
static long total, failed;
/* Checks both families and both case modes against two answer strings. */
static void check(ScrStr *path, ScrStr *pattern, const char *posix, const char *win32, const char *path_text,
const char *pattern_text) {
for (int windows = 0; windows < 2; windows++) {
const char *expected = windows ? win32 : posix;
for (int nocase = 0; nocase < 2; nocase++) {
bool hit = scr_glob_match(path, pattern, windows, nocase);
char got = scr_exc_pending() ? 'E' : hit ? '1' : '0';
if (scr_exc_pending()) scr_exc_clear();
total++;
if (got != expected[nocase]) {
failed++;
if (failed <= 40)
fprintf(stderr, "MISMATCH %s nocase=%d path=\"%s\" pattern=\"%s\" got %c want %c\n",
windows ? "win32" : "posix", nocase, path_text, pattern_text, got, expected[nocase]);
}
}
}
}
int main(int argc, char **argv) {
if (argc < 2) return 2;
FILE *f = fopen(argv[1], "r");
if (!f) return 2;
static char line[1 << 20];
size_t npaths = 0, cap = 0;
ScrStr **paths = NULL;
char **path_texts = NULL;
while (fgets(line, sizeof line, f)) {
size_t len = strlen(line);
if (len && line[len - 1] == '\n') line[--len] = 0;
if (!len) continue;
char *fields[5];
int count = split(line, fields, 5);
if (strcmp(fields[0], "p") == 0 && count == 2) {
if (npaths == cap) {
cap = cap ? 2 * cap : 64;
paths = realloc(paths, cap * sizeof *paths);
path_texts = realloc(path_texts, cap * sizeof *path_texts);
}
path_texts[npaths] = strdup(fields[1]);
paths[npaths++] = decode(fields[1]);
} else if (strcmp(fields[0], "c") == 0 && count == 4 && strlen(fields[2]) == 2 * npaths &&
strlen(fields[3]) == 2 * npaths) {
ScrStr *pattern = decode(fields[1]);
for (size_t i = 0; i < npaths; i++)
check(paths[i], pattern, fields[2] + 2 * i, fields[3] + 2 * i, path_texts[i], fields[1]);
scr_str_release(pattern);
} else if (strcmp(fields[0], "f") == 0 && count == 5 && strlen(fields[3]) == 2 && strlen(fields[4]) == 2) {
ScrStr *path = decode(fields[1]);
ScrStr *pattern = decode(fields[2]);
check(path, pattern, fields[3], fields[4], fields[1], fields[2]);
scr_str_release(path);
scr_str_release(pattern);
} else {
fprintf(stderr, "bad line\n");
return 2;
}
}
fclose(f);
for (size_t i = 0; i < npaths; i++) {
scr_str_release(paths[i]);
free(path_texts[i]);
}
free(paths);
free(path_texts);
#ifdef SCR_RC_AUDIT
if (scr_str_live_count() != 0) {
fprintf(stderr, "live strings at exit: %ld\n", scr_str_live_count());
failed++;
}
#endif
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
return failed == 0 ? 0 : 1;
}