diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aea56e495..cbbaf68d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,28 @@ a focused subset with: ./rayforce.test -f ``` +A `.rfl` file that needs a POSIX shell or filesystem (fixtures built or +checked through `.sys.exec`, `/proc`, `/dev/tcp`, …) carries the line +`;; @requires: posix`; on Windows the runner reports it as `SKIP` rather +than running it. C tests use `#ifndef RAY_OS_WINDOWS` / `SKIP(...)` for the +same purpose. Prefer the shell-free helpers `ray_test_rm_rf` / +`ray_test_mkdir_p` (`test/test.h`) over `system("rm -rf …")` in C tests. + +### Windows + +Build with the MSYS2 CLANG64 (or MINGW64) toolchain — `pacman -S +mingw-w64-clang-x86_64-clang make` — from an MSYS2 shell or with +`C:\msys64\clang64\bin` and `C:\msys64\usr\bin` on `PATH`: + +```sh +make # debug build (ASan + UBSan) +make test +make release +``` + +The debug binaries load the ASan runtime DLL from `clang64\bin`, so keep it on +`PATH` when running them. + ## Stability tooling Beyond the ASan/UBSan test run, the repo carries a stability toolset. These diff --git a/Makefile b/Makefile index aa6b666e9..f6c170f30 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,25 @@ COVERAGE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -g -O0 -march=$(RAY_MARCH) -DDEBUG -fno-omit-frame-pointer -fprofile-instr-generate -fcoverage-mapping COVERAGE_LDFLAGS = -fprofile-instr-generate -fcoverage-mapping -ifeq ($(UNAME_S),Linux) +# Windows: MSYS2 CLANG64/MINGW64 toolchain (x86_64-w64-windows-gnu). MSYS2's +# own make hides $(OS), so also match `uname -s` (MINGW64_NT-*, CLANG64_NT-*, +# MSYS_NT-*); a native make started from PowerShell/cmd sees OS=Windows_NT. +RAY_WINDOWS := $(if $(filter Windows_NT,$(OS))$(findstring _NT-,$(UNAME_S)),1,) + +ifeq ($(RAY_WINDOWS),1) + # 64-bit off_t / struct stat.st_size: MinGW defaults both to 32 bits, which + # silently truncates sizes of files over 2 GiB (stat, lseek, ftruncate). + DEFS += -D_FILE_OFFSET_BITS=64 + # --stack: 8 MiB like a Linux main thread (the Windows default is 1 MiB, + # too little for deep DAG/eval recursion). CreateThread(size 0) inherits + # it too, so pool workers get the same. winpthreads (sched_yield, + # clock_gettime) is linked statically so the binary needs no MSYS2 DLL; the + # UCRT it also uses ships with Windows 10+. + LIBS = -lws2_32 -lmswsock -lkernel32 -ladvapi32 \ + -Wl,-Bstatic -lpthread -Wl,-Bdynamic \ + -Wl,--stack,8388608 + RELEASE_LDFLAGS = -Wl,--gc-sections +else ifeq ($(UNAME_S),Linux) LIBS = -lm -lpthread RELEASE_LDFLAGS = -Wl,--gc-sections -Wl,--as-needed else diff --git a/RELEASE.md b/RELEASE.md index fb141e6e8..76421ebe8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -136,7 +136,7 @@ Each release publishes, in addition to the source: ## Platform support -Linux and macOS binaries are published today. Windows is not build-ready yet -(IOCP backend is a stub, `main.c`/`heap.c` have unguarded POSIX calls, and the -Makefile has no Windows toolchain path); once ported, add a `windows-latest` row -to the `build` matrix in `.github/workflows/release.yml`. +Linux and macOS binaries are published today. Windows builds and passes the +test suite from source with the MSYS2 CLANG64 toolchain (see CONTRIBUTING.md), +but no Windows binary is published yet; to ship one, add a `windows-latest` +row (MSYS2 CLANG64) to the `build` matrix in `.github/workflows/release.yml`. diff --git a/bench/agg_v2/main.c b/bench/agg_v2/main.c index 401365ce9..908fc3f44 100644 --- a/bench/agg_v2/main.c +++ b/bench/agg_v2/main.c @@ -46,7 +46,13 @@ #include #include #include -#include +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif /* ---------- timing ---------- */ static double now_ms(void) { @@ -69,12 +75,18 @@ static double vmin(const double* arr, int n) { return m; } static long max_rss_kb(void) { +#if defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) return 0; + return (long)(pmc.PeakWorkingSetSize / 1024); +#else struct rusage ru; getrusage(RUSAGE_SELF, &ru); #if defined(__APPLE__) return ru.ru_maxrss / 1024; #else return ru.ru_maxrss; #endif +#endif } /* ---------- deterministic PRNG (splitmix64) ---------- */ diff --git a/bench/alloc/main.c b/bench/alloc/main.c index 9bbefc124..3294e9189 100644 --- a/bench/alloc/main.c +++ b/bench/alloc/main.c @@ -11,7 +11,13 @@ #include #include #include -#include +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif static double now_s(void) { struct timespec ts; @@ -66,6 +72,11 @@ static void* consumer(void* _) { } static long max_rss_kb(void) { +#if defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) return 0; + return (long)(pmc.PeakWorkingSetSize / 1024); +#else struct rusage ru; getrusage(RUSAGE_SELF, &ru); /* Linux: ru_maxrss is KB; macOS: bytes. Normalize to KB. */ #if defined(__APPLE__) @@ -73,6 +84,7 @@ static long max_rss_kb(void) { #else return ru.ru_maxrss; #endif +#endif } int main(void) { diff --git a/bench/bottleneck/windows_vs_linux.md b/bench/bottleneck/windows_vs_linux.md new file mode 100644 index 000000000..c7b0e30a7 --- /dev/null +++ b/bench/bottleneck/windows_vs_linux.md @@ -0,0 +1,92 @@ +# Windows vs Linux sanity check + +Not a performance study: a coarse check that the Windows port lands in the +same ballpark as Linux, run while porting (`serhii/windows-port`). The two +sides do not share a compiler, an allocator-visible kernel, or a filesystem, +so only order-of-magnitude gaps are meaningful here. + +## Environment + +**CPU**: 11th Gen Intel Core i7 (8 logical cores) — one laptop, both runs +**Windows**: Windows 11, clang 20.1.8 (MSYS2 CLANG64), 32 GiB +**Linux**: WSL2 (kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04), gcc 13.3.0, 15 GiB to the VM +**Build**: `make release` both sides (`-O3 -march=native`, no sanitizers — `nm bench-alloc | grep -ci asan` → 0) + +WSL2 is a virtual machine with its own memory budget and a virtualised +filesystem; that alone moves I/O and page-fault numbers. Treat the file-backed +rows as indicative only. + +## Allocator micro-benchmark (`bench/alloc`) + +| case | Windows | Linux | +|------|---------|-------| +| atom-64B | 85.7 Mops/s | 80.9 Mops/s | +| vec-256B | 85.5 Mops/s | 78.6 Mops/s | +| morsel-8K | 88.2 Mops/s | 80.3 Mops/s | +| morsel-16K | 88.4 Mops/s | 78.2 Mops/s | +| large-1M | 62.7 Mops/s | 56.4 Mops/s | +| producer-consumer | 9.2 Mops/s, peak RSS 28 MB | 6.0 Mops/s, peak RSS 68 MB | + +## Engine operations (5M rows, `timeit`, median of 3) + +| operation | Windows (ms) | Linux (ms) | Win/Lin | +|-----------|-------------:|-----------:|--------:| +| arith-f64 (`sum (* f 1.5)`) | 7.94 | 5.10 | 1.56 | +| sort-i64 | 9.89 | 8.78 | 1.13 | +| distinct-i64 | 2.96 | 4.69 | 0.63 | +| group-by sym | 3.44 | 3.93 | 0.88 | +| select where | 7.13 | 14.14 | 0.50 | +| inner-join | 108.6 | 108.3 | 1.00 | +| csv write (5M rows) | 2201 | 1654 | 1.33 | +| csv read | 155 | 186 | 0.83 | +| splayed set | 509 | 580 | 0.88 | +| splayed get + count | 5.9 | 0.28 | 21 | + +`sum`/`avg` are omitted: both platforms report ~0.005 ms, the DAG elides them. + +## Reading + +Compute paths agree within a factor of ~1.6 either way, which is compiler and +noise territory, and the allocator is slightly ahead on Windows. + +The one real gap is `splayed get + count` (5.9 ms vs 0.28 ms). It opens and +maps one file per column, so it measures file-open cost, not the engine: +`CreateFileA` + `CreateFileMapping` + `MapViewOfFile` per column, plus +whatever on-access scanning is installed. The absolute cost is small and it is +paid per table open, but a wide table opened in a loop would feel it. + +## The benches recent PRs shipped + +Same binaries, built from the release objects on each side. + +**`bench/join_nullfree` (#598, null-free key fast path).** The optimisation +engages on Windows — the `nullfree` counter advances on the null-free cases +and stays put on the nullable one, as on Linux. + +| case | Windows median (baseline → fast) | Linux median | +|------|---------------------------------|--------------| +| SYM2 | 355.3 → 336.0 ms (-5.4%) | 413.6 → 408.4 ms (-1.3%) | +| SYM2-NULL (must not fire) | 348.2 → 349.8 ms (+0.5%) | 407.1 → 412.9 ms (+1.4%) | +| I64 | 102.7 → 89.2 ms (-13.2%) | 100.9 → 96.3 ms (-4.6%) | + +**`bench/join_dup` (duplicate-key fallback).** The pathological case is fixed +on both: CATASTROPHIC-INNER post-fix ~170 ms on Windows and ~230 ms on Linux, +against ~2.6 s pre-fix (Windows) — the same order-of-magnitude win. + +**`bench/join_buildside` (build-side swap).** The swap fires on Windows and +pays off by the same factor: MANY-TO-MANY 207 ms swapped vs 494 ms legacy +(2.4x); Linux 188 vs 431 (2.3x). HEAVY-DUP-WIN: 1.7 s vs 6.2 s (Windows), +1.8 s vs 8.2 s (Linux). + +**`bench/idx_route` Q3** (1000 lookups/rep): indexed 0.014 ms/batch on +Windows, 0.009 on Linux; the unindexed control is 0.004 on both. + +Not runnable as-is: + +- `bench/group_pushdown` and `bench/agg_v2` no longer compile **on either + platform** — they use `ray_op.inputs` and `ray_group2/3`, which the engine + no longer has. Pre-existing, unrelated to the port. +- `bench/groupby_shapes/*.py` needs python3, which a stock Windows lacks; the + `.rfl` cases in that directory run directly under `rayforce` on both. +- `scripts/soak.sh` and `scripts/fuzz-seed-*.sh` are bash and stay POSIX-only + (the fuzzing runtime is Linux-only anyway, see the Makefile). diff --git a/docs/docs/getting-started/quick-start.md b/docs/docs/getting-started/quick-start.md index 73a2dc04a..143da2053 100644 --- a/docs/docs/getting-started/quick-start.md +++ b/docs/docs/getting-started/quick-start.md @@ -58,7 +58,7 @@ The Rayfall REPL provides an interactive environment with syntax highlighting, b ./rayforce ``` -You will see the `‣` prompt (a green triangle bullet): +You will see the `‣` prompt (a green triangle bullet; `►` on Windows, whose console fonts have no `‣`): ```text ‣ diff --git a/docs/docs/guides/memory.md b/docs/docs/guides/memory.md index 998c1d2d6..a0050195e 100644 --- a/docs/docs/guides/memory.md +++ b/docs/docs/guides/memory.md @@ -104,10 +104,6 @@ total-mem | 16777216000 | `page-size` | OS page size in bytes | | `total-mem` | Total physical RAM in bytes | -!!! note "Note" - - On Windows, only `cores` is currently reported. - ## 5. Progress Monitoring Long-running queries display a progress bar automatically in the REPL. The bar appears after approximately 2 seconds of execution and shows real-time feedback. diff --git a/docs/docs/index.md b/docs/docs/index.md index 6550f46a3..162253e75 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -2,7 +2,7 @@ Embeddable columnar analytics and graph traversal engine in pure C. -Rayforce combines morsel-driven vectorized execution, a multi-pass query optimizer, and a native CSR graph engine in a single pipeline. It is queried through the **Rayfall** language, exposes a C API for embedding, and runs on Linux and macOS (Windows is planned — the IOCP backend is still a stub). +Rayforce combines morsel-driven vectorized execution, a multi-pass query optimizer, and a native CSR graph engine in a single pipeline. It is queried through the **Rayfall** language, exposes a C API for embedding, and runs on Linux and macOS, and builds on Windows from source (MSYS2 CLANG64). [Quick Start](getting-started/quick-start.md){ .md-button .md-button--primary } [Functions Reference](language/functions.md){ .md-button } diff --git a/docs/docs/namespaces/sys.md b/docs/docs/namespaces/sys.md index 5e13d189b..f6eefbffb 100644 --- a/docs/docs/namespaces/sys.md +++ b/docs/docs/namespaces/sys.md @@ -75,7 +75,7 @@ Signature: `(.sys.build)`. Returns a dict with `version` (string) and `build-dat ## `.sys.info` { #sys-info } -Signature: `(.sys.info)`. Returns `{cores: i64, page-size: i64, total-mem: i64, pid: i64, hostname: str}` on POSIX. On Windows the machine facts fall back to `{cores: 1}` (the sysconf-backed values aren't wired), but `pid` and `hostname` are answered on both platforms. +Signature: `(.sys.info)`. Returns `{cores: i64, page-size: i64, total-mem: i64, pid: i64, hostname: str}` on every platform (on Windows from `GetSystemInfo` / `GlobalMemoryStatusEx`). ```lisp (.sys.info) diff --git a/src/app/repl.c b/src/app/repl.c index 6775c8909..b5cd32d86 100644 --- a/src/app/repl.c +++ b/src/app/repl.c @@ -51,6 +51,7 @@ #include #include #include +#include #if defined(RAY_OS_WINDOWS) #include @@ -59,7 +60,6 @@ #define STDIN_FD 0 #else #include -#include #include #define STDIN_FD STDIN_FILENO #endif @@ -353,7 +353,18 @@ static void get_cpu_name(char* buf, size_t sz) { if (sysctlbyname("machdep.cpu.brand_string", buf, &len, NULL, 0) != 0) snprintf(buf, sz, "unknown"); #elif defined(RAY_OS_WINDOWS) - snprintf(buf, sz, "unknown"); + /* The brand string the firmware reported, same text as /proc/cpuinfo's + * "model name"; it is padded with trailing spaces, so trim them. */ + DWORD n = (DWORD)sz; + if (RegGetValueA(HKEY_LOCAL_MACHINE, + "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", + "ProcessorNameString", RRF_RT_REG_SZ, NULL, + buf, &n) == ERROR_SUCCESS) { + size_t len = strlen(buf); + while (len > 0 && buf[len - 1] == ' ') buf[--len] = '\0'; + } else { + snprintf(buf, sz, "unknown"); + } #else snprintf(buf, sz, "unknown"); #endif @@ -385,7 +396,11 @@ static void print_banner(void) { char cpu[256]; get_cpu_name(cpu, sizeof(cpu)); int64_t mem_mb = get_total_mem_mb(); +#if defined(RAY_OS_WINDOWS) + int ncores = (int)ray_thread_count(); +#else int ncores = (int)sysconf(_SC_NPROCESSORS_ONLN); +#endif /* "Using" count reflects the actual worker-pool size, not ncores. * ray_pool_get() is a lazy initializer — callers might not have diff --git a/src/app/term.c b/src/app/term.c index ffae95482..3b1560a69 100644 --- a/src/app/term.c +++ b/src/app/term.c @@ -67,7 +67,12 @@ typedef struct stat hist_stat_t; #define RAY_BLOCK_FROM_DATA(ptr) ((ray_t*)((char*)(ptr) - sizeof(ray_t))) /* Suppress -Wunused-result for terminal I/O writes to stdout. */ -#if !defined(RAY_OS_WINDOWS) +#if defined(RAY_OS_WINDOWS) +static inline void term_write(const void* buf, size_t len) { + int r = _write(1, buf, (unsigned)len); + (void)r; +} +#else static inline void term_write(const void* buf, size_t len) { ssize_t r = write(STDOUT_FILENO, buf, len); (void)r; @@ -1512,8 +1517,18 @@ int32_t ray_term_count_unmatched(ray_term_t* term) { /* ===== Prompt ===== */ -/* Green ‣ (U+2023) prompt, matching Rayforce style */ +/* Green ‣ (U+2023) prompt, matching Rayforce style. + * + * No console font Windows ships has U+2023 — not Consolas, Cascadia Mono, + * Lucida Console or Courier New — and the classic console does no font + * fallback, so the prompt renders as "?" there. Windows uses ► (U+25BA), + * the nearest filled triangle all four of them do have; it is also three + * UTF-8 bytes, so the byte and visual widths below are unchanged. */ +#if defined(RAY_OS_WINDOWS) +#define PROMPT_STR "\033[32m\xe2\x96\xba\033[0m " +#else #define PROMPT_STR "\033[32m\xe2\x80\xa3\033[0m " +#endif #define PROMPT_LEN 13 /* ESC[32m (5) + ‣ (3) + ESC[0m (4) + space (1) = 13 bytes */ #define PROMPT_VIS 2 /* visual: ‣ + space */ #define CONT_PROMPT_STR "\033[90m\xe2\x80\xa6\033[0m " /* gray … (U+2026) */ diff --git a/src/app/term.h b/src/app/term.h index cf89457bf..8c7f23fa3 100644 --- a/src/app/term.h +++ b/src/app/term.h @@ -24,9 +24,12 @@ #ifndef RAY_TERM_H #define RAY_TERM_H -#include +#include "core/platform.h" #if defined(RAY_OS_WINDOWS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN /* keep / macros out */ +#endif #include #define KEYCODE_RETURN '\r' #else diff --git a/src/core/crash.c b/src/core/crash.c index 17d644edb..41da28900 100644 --- a/src/core/crash.c +++ b/src/core/crash.c @@ -19,10 +19,14 @@ #include "core/crash.h" #include +#include +#include #include #include -#if !defined(_WIN32) +#if defined(_WIN32) +#include +#else #include #endif @@ -42,7 +46,7 @@ static void cw(const char* s) { /* Write an unsigned value as 0x-prefixed hex. No libc formatting. * Routes through cw() so the write return value is handled (gcc's * warn_unused_result on write() is not silenced by a (void) cast). */ -static void cw_hex(unsigned long v) { +static void cw_hex(uint64_t v) { char buf[2 + 16 + 1]; static const char hexd[] = "0123456789abcdef"; buf[0] = '0'; buf[1] = 'x'; @@ -52,7 +56,8 @@ static void cw_hex(unsigned long v) { cw(buf); } -/* Write a small non-negative integer as decimal. */ +#if !defined(_WIN32) +/* Write a small non-negative integer as decimal (backtrace frame count). */ static void cw_int(int v) { if (v < 0) { cw("-"); v = -v; } char buf[16]; @@ -62,6 +67,74 @@ static void cw_int(int v) { while (v > 0 && i > 0) { buf[--i] = (char)('0' + v % 10); v /= 10; } cw(&buf[i]); } +#endif + +/* Banner precomputed at install time so the handler doesn't format it. */ +static char g_banner[128]; + +#ifndef RAYFORCE_VERSION +#define RAYFORCE_VERSION "" +#endif +#ifndef RAYFORCE_GIT_COMMIT +#define RAYFORCE_GIT_COMMIT "" +#endif + +static void crash_banner_init(void) { + const char* ver = RAYFORCE_VERSION; + const char* rev = RAYFORCE_GIT_COMMIT; + int n = snprintf(g_banner, sizeof(g_banner), "rayforce%s%s%s%s%s\n", + ver[0] ? " " : "", ver, + rev[0] ? " (" : "", rev, rev[0] ? ")" : ""); + if (n < 0) g_banner[0] = '\0'; +} + +#if defined(_WIN32) + +/* ── Windows: structured exceptions ───────────────────────────────── */ + +static const char* exc_name(DWORD code) { + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_IN_PAGE_ERROR: return "EXCEPTION_IN_PAGE_ERROR"; + default: return "exception"; + } +} + +/* Top-level filter: runs only for exceptions nobody else handled. Report + * and let the default handling (WER / exit with the exception code) run, + * which keeps the process exit status meaningful to the orchestrator. */ +static LONG WINAPI crash_filter(EXCEPTION_POINTERS* ep) { + const EXCEPTION_RECORD* er = ep ? ep->ExceptionRecord : NULL; + cw("\n=== rayforce fatal "); + cw(er ? exc_name(er->ExceptionCode) : "exception"); + if (er) { + cw(" code "); cw_hex((uint64_t)er->ExceptionCode); + cw(" at "); cw_hex((uint64_t)(uintptr_t)er->ExceptionAddress); + if (er->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && + er->NumberParameters >= 2) { + cw(" fault addr "); + cw_hex((uint64_t)er->ExceptionInformation[1]); + } + } + cw(" ===\n"); + cw(g_banner); + return EXCEPTION_CONTINUE_SEARCH; +} + +void ray_crash_install(void) { + crash_banner_init(); + /* Reserve stack for the filter itself, so a stack-overflow exception + * can still be reported (the Windows analogue of sigaltstack). */ + ULONG guarantee = 64 * 1024; + (void)SetThreadStackGuarantee(&guarantee); + SetUnhandledExceptionFilter(crash_filter); +} + +#else /* POSIX */ static const char* sig_name(int sig) { switch (sig) { @@ -74,9 +147,6 @@ static const char* sig_name(int sig) { } } -/* Banner precomputed at install time so the handler doesn't format it. */ -static char g_banner[128]; - /* ── the handler ──────────────────────────────────────────────────── */ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { @@ -84,11 +154,10 @@ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { cw("\n=== rayforce fatal "); cw(sig_name(sig)); - if (info) { cw(" at fault addr "); cw_hex((unsigned long)info->si_addr); } + if (info) { cw(" at fault addr "); cw_hex((uint64_t)(uintptr_t)info->si_addr); } cw(" ===\n"); cw(g_banner); -#if !defined(_WIN32) void* frames[64]; int n = backtrace(frames, 64); /* backtrace_symbols_fd writes directly to the fd without allocating. */ @@ -96,7 +165,6 @@ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { cw("=== end backtrace ("); cw_int(n); cw(" frames) ===\n"); -#endif /* Restore the default disposition and re-raise, so the process dies * from the original signal: this preserves the core dump and reports @@ -116,24 +184,8 @@ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { static char g_altstack[RAY_CRASH_ALTSTACK_SZ]; void ray_crash_install(void) { -#if !defined(_WIN32) /* Precompute the version banner once (async-signal-safe reuse). */ - { - const char* v = -#ifdef RAYFORCE_VERSION - "rayforce " RAYFORCE_VERSION -#else - "rayforce" -#endif -#ifdef RAYFORCE_GIT_COMMIT - " (" RAYFORCE_GIT_COMMIT ")" -#endif - "\n"; - size_t vl = strlen(v); - if (vl >= sizeof(g_banner)) vl = sizeof(g_banner) - 1; - memcpy(g_banner, v, vl); - g_banner[vl] = '\0'; - } + crash_banner_init(); /* Warm up the unwinder: the first backtrace() may dlopen libgcc and * allocate, which must not happen inside the handler. */ @@ -157,5 +209,6 @@ void ray_crash_install(void) { static const int sigs[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT }; for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) (void)sigaction(sigs[i], &sa, NULL); -#endif } + +#endif /* _WIN32 */ diff --git a/src/core/iocp.c b/src/core/iocp.c index 636c3fed1..dd75fd8f3 100644 --- a/src/core/iocp.c +++ b/src/core/iocp.c @@ -21,36 +21,181 @@ * SOFTWARE. */ +#include "core/platform.h" + #if defined(RAY_OS_WINDOWS) +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + #include "core/poll.h" -#include +#include "core/mcast.h" +#include "core/timer.h" +#include "mem/sys.h" +#include "mem/heap.h" /* idle decay: bound the wait, sweep after wakeup */ + +/* Windows event loop. + * + * Readiness-based, like the epoll and kqueue backends, so the selector + * state machine (rx fill -> read_fn -> data_fn, tx flush) is the same on + * every platform. Sockets are waited on with WSAPoll. Standard input is + * not a socket and WSAPoll cannot watch it, so RAY_SEL_STDIN selectors are + * probed directly (console input queue, bytes in a pipe) and the socket + * wait is cut into short slices while one is registered. + * + * WSAPoll keeps no kernel-side registration, so the wait set is rebuilt + * from poll->sels on every pass: register/deregister and tx request/cancel + * need no OS call, and a selector's write interest simply follows whether + * it has a pending tx buffer. */ + +#define RAY_POLL_INITIAL_CAP 16 +#define RAY_POLL_STDIN_SLICE_MS 10 + +enum { EV_IN = 1, EV_OUT = 2, EV_HUP = 4 }; + +/* ===== stdin readiness ===== */ + +/* Would a read of `fd` return now (data or EOF) instead of blocking? */ +static int stdin_events(int64_t fd) +{ + HANDLE h = (HANDLE)_get_osfhandle((int)fd); + if (h == INVALID_HANDLE_VALUE) return EV_HUP; + + switch (GetFileType(h)) { + case FILE_TYPE_CHAR: { + DWORD mode; + if (!GetConsoleMode(h, &mode)) return EV_IN; /* NUL device: never blocks */ + for (;;) { + INPUT_RECORD rec; + DWORD n = 0; + if (!PeekConsoleInputW(h, &rec, 1, &n)) return EV_HUP; + if (n == 0) return 0; + if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown && + rec.Event.KeyEvent.uChar.UnicodeChar != 0) + return EV_IN; + /* Focus, mouse, resize and key-up records never yield a byte to + * ReadFile: drop them, or a "ready" console would make the + * reader block in ReadFile until the next real keystroke. */ + if (!ReadConsoleInputW(h, &rec, 1, &n)) return EV_HUP; + } + } + case FILE_TYPE_PIPE: { + DWORD avail = 0; + if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) + return EV_IN | EV_HUP; /* writer gone: the read returns EOF */ + return avail ? EV_IN : 0; + } + default: + return EV_IN; /* disk file: reads never block */ + } +} -/* Windows IOCP implementation — stub for now. - * Full IOCP support is deferred to a future release. */ +/* ===== Lifecycle ===== */ ray_poll_t* ray_poll_create(void) { - fprintf(stderr, "ray_poll_create: IOCP not yet implemented\n"); - return NULL; + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return NULL; + + ray_poll_t* poll = (ray_poll_t*)ray_sys_alloc(sizeof(ray_poll_t)); + if (!poll) return NULL; + + memset(poll, 0, sizeof(*poll)); + poll->fd = -1; /* no kernel object behind a WSAPoll loop */ + poll->code = -1; + poll->sel_cap = RAY_POLL_INITIAL_CAP; + poll->sels = (ray_selector_t**)ray_sys_alloc( + poll->sel_cap * sizeof(ray_selector_t*)); + if (!poll->sels) { + ray_sys_free(poll); + return NULL; + } + memset(poll->sels, 0, poll->sel_cap * sizeof(ray_selector_t*)); + return poll; } void ray_poll_destroy(ray_poll_t* poll) { - (void)poll; + if (!poll) return; + + for (uint32_t i = 0; i < poll->n_sels; i++) { + ray_selector_t* sel = poll->sels[i]; + if (!sel) continue; + if (sel->close_fn) sel->close_fn(poll, sel); + if (sel->rx.buf) ray_poll_buf_free(sel->rx.buf); + ray_poll_buf_free(sel->tx.buf); + ray_sys_free(sel); + poll->sels[i] = NULL; + } + + if (poll->sels) ray_sys_free(poll->sels); + if (poll->timers) { + ray_timers_destroy((ray_timers_t*)poll->timers); + poll->timers = NULL; + } + if (poll->mcast) { + ray_mcast_destroy((ray_mcast_t*)poll->mcast); + poll->mcast = NULL; + } + ray_sys_free(poll); } +/* ===== Registration ===== */ + int64_t ray_poll_register(ray_poll_t* poll, ray_poll_reg_t* reg) { - (void)poll; (void)reg; - return -1; -} + if (!poll || !reg) return -1; -void ray_poll_deregister(ray_poll_t* poll, int64_t id) -{ - (void)poll; (void)id; + /* Find free slot or grow */ + int64_t id = -1; + for (uint32_t i = 0; i < poll->n_sels; i++) { + if (!poll->sels[i]) { id = (int64_t)i; break; } + } + if (id < 0) { + if (poll->n_sels >= poll->sel_cap) { + uint32_t new_cap = poll->sel_cap * 2; + ray_selector_t** ns = (ray_selector_t**)ray_sys_alloc( + new_cap * sizeof(ray_selector_t*)); + if (!ns) return -1; + memcpy(ns, poll->sels, poll->n_sels * sizeof(ray_selector_t*)); + memset(ns + poll->n_sels, 0, + (new_cap - poll->n_sels) * sizeof(ray_selector_t*)); + ray_sys_free(poll->sels); + poll->sels = ns; + poll->sel_cap = new_cap; + } + id = (int64_t)poll->n_sels; + poll->n_sels++; + } + + ray_selector_t* sel = (ray_selector_t*)ray_sys_alloc(sizeof(ray_selector_t)); + if (!sel) return -1; + memset(sel, 0, sizeof(*sel)); + + sel->fd = reg->fd; + sel->id = id; + sel->type = reg->type; + sel->data = reg->data; + sel->open_fn = reg->open_fn; + sel->close_fn = reg->close_fn; + sel->error_fn = reg->error_fn; + sel->data_fn = reg->data_fn; + sel->rx.recv_fn = reg->recv_fn; + sel->rx.read_fn = reg->read_fn; + sel->tx.send_fn = reg->send_fn; + + poll->sels[id] = sel; + poll->n_live++; + if (sel->open_fn) sel->open_fn(poll, sel); + return id; } +/* Write interest is derived from sel->tx.buf when the wait set is built. */ void ray_poll_tx_request(ray_poll_t* poll, ray_selector_t* sel) { (void)poll; (void)sel; @@ -61,10 +206,266 @@ void ray_poll_tx_cancel(ray_poll_t* poll, ray_selector_t* sel) (void)poll; (void)sel; } +void ray_poll_deregister(ray_poll_t* poll, int64_t id) +{ + if (!poll || id < 0 || (uint32_t)id >= poll->n_sels) return; + ray_selector_t* sel = poll->sels[id]; + if (!sel) return; + + if (sel->close_fn) sel->close_fn(poll, sel); + if (sel->rx.buf) ray_poll_buf_free(sel->rx.buf); + ray_poll_buf_free(sel->tx.buf); + ray_sys_free(sel); + poll->sels[id] = NULL; + if (poll->n_live > 0) poll->n_live--; +} + +/* ===== Event dispatch (same order and rules as the epoll backend) ===== */ + +static void poll_dispatch(ray_poll_t* poll, uint64_t eid, int events) +{ + ray_selector_t* sel = NULL; + if (eid < poll->n_sels) + sel = poll->sels[eid]; + if (!sel) return; + + /* Process readable data first — even if hangup is also set. A client + * may send a message and close; both arrive in the same wakeup. */ + if (events & EV_IN) { + /* Loop: read data -> call read_fn -> if state advanced, read more. + * Handles multi-phase protocols (handshake -> header -> payload) + * arriving in a single wakeup. */ + for (;;) { + if (sel->rx.recv_fn && sel->rx.buf) { + while (sel->rx.buf->offset < sel->rx.buf->size) { + int64_t nr = sel->rx.recv_fn( + sel->fd, + sel->rx.buf->data + sel->rx.buf->offset, + sel->rx.buf->size - sel->rx.buf->offset); + if (nr <= 0) { + if (nr < 0 && errno == EINTR) continue; + if (nr < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + /* Error or peer closed mid-read */ + if (sel->error_fn) + sel->error_fn(poll, sel); + else + ray_poll_deregister(poll, sel->id); + return; + } + sel->rx.buf->offset += nr; + } + } + + /* Not enough data for current phase */ + if (sel->rx.buf && sel->rx.buf->offset < sel->rx.buf->size) + break; + + /* Call read_fn — may advance state and request new buffer */ + if (!sel->rx.read_fn) break; + ray_t* obj = sel->rx.read_fn(poll, sel); + + /* Re-validate: read_fn may have deregistered this selector */ + if (eid >= poll->n_sels || !poll->sels[eid]) return; + sel = poll->sels[eid]; + + if (obj && sel->data_fn) + sel->data_fn(poll, sel, obj); + + if (eid >= poll->n_sels || !poll->sels[eid]) return; + sel = poll->sels[eid]; + + /* If no rx buffer (state machine done or not set), stop */ + if (!sel->rx.buf) break; + /* If buffer already has enough data for next phase, loop */ + if (sel->rx.buf->offset >= sel->rx.buf->size) continue; + /* Otherwise try reading more (may EAGAIN -> break) */ + } + } + + if (events & EV_OUT) { + if (eid >= poll->n_sels || !poll->sels[eid]) return; + sel = poll->sels[eid]; + if (sel->tx.buf && ray_poll_tx_flush(poll, sel) < 0) { + if (eid < poll->n_sels && poll->sels[eid]) { + sel = poll->sels[eid]; + if (sel->error_fn) + sel->error_fn(poll, sel); + else + ray_poll_deregister(poll, sel->id); + } + return; + } + } + + /* Error / hangup — after data is drained */ + if (events & EV_HUP) { + if (eid < poll->n_sels && poll->sels[eid]) { + sel = poll->sels[eid]; + if (sel->error_fn) + sel->error_fn(poll, sel); + else + ray_poll_deregister(poll, sel->id); + } + } +} + +/* ===== Run loop ===== */ + int64_t ray_poll_run_for(ray_poll_t* poll, int timeout_ms) { - (void)poll; (void)timeout_ms; - return -1; + if (!poll) return -1; + + bool bounded = timeout_ms >= 0; + int64_t end_ms = bounded ? ray_time_now_ms() + timeout_ms : INT64_MAX; + + while (poll->code < 0) { + int wait_ms = -1; + if (bounded) { + int64_t remaining = end_ms - ray_time_now_ms(); + if (remaining < 0) remaining = 0; + if (remaining > INT_MAX) remaining = INT_MAX; + wait_ms = (int)remaining; + } + + /* Nothing registered and nothing scheduled: an unbounded loop + * would block here forever. Return instead, so a process that + * stayed only for its timers can end once they are spent. */ + if (!bounded && ray_poll_idle(poll)) return 0; + + if (poll->timers) { + int64_t deadline = ray_timers_next_deadline_ms( + (ray_timers_t*)poll->timers); + if (deadline != INT64_MAX) { + int64_t delta = deadline - ray_time_now_ms(); + if (delta < 0) delta = 0; + if (delta > INT_MAX) delta = INT_MAX; + if (wait_ms < 0 || delta < wait_ms) + wait_ms = (int)delta; + } + } + + /* Idle allocator decay bounds an unbounded wait, exactly as in the + * epoll backend (see the comment there). */ + { + int64_t decay = bounded ? -1 : ray_heap_decay_due_ms(); + if (decay >= 0) { + if (decay > INT_MAX) decay = INT_MAX; + if (wait_ms < 0 || decay < wait_ms) wait_ms = (int)decay; + } + } + + /* Build this pass's wait set: sockets for WSAPoll, stdin probed. */ + uint32_t cap = poll->n_sels ? poll->n_sels : 1; + WSAPOLLFD* pfds = (WSAPOLLFD*)ray_sys_alloc(cap * sizeof(WSAPOLLFD)); + uint32_t* pids = (uint32_t*)ray_sys_alloc(cap * sizeof(uint32_t)); + uint32_t* sids = (uint32_t*)ray_sys_alloc(cap * sizeof(uint32_t)); + int* sevs = (int*)ray_sys_alloc(cap * sizeof(int)); + if (!pfds || !pids || !sids || !sevs) { + if (pfds) ray_sys_free(pfds); + if (pids) ray_sys_free(pids); + if (sids) ray_sys_free(sids); + if (sevs) ray_sys_free(sevs); + return -1; + } + + uint32_t nfds = 0, nstd = 0; + for (uint32_t i = 0; i < poll->n_sels; i++) { + ray_selector_t* sel = poll->sels[i]; + if (!sel) continue; + if (sel->type == RAY_SEL_STDIN) { + sids[nstd] = i; + sevs[nstd] = 0; + nstd++; + continue; + } + pfds[nfds].fd = (SOCKET)sel->fd; + pfds[nfds].events = POLLRDNORM | (sel->tx.buf ? POLLWRNORM : 0); + pfds[nfds].revents = 0; + pids[nfds] = i; + nfds++; + } + + /* Wait. With stdin registered the socket wait is sliced so the + * console/pipe is re-probed every RAY_POLL_STDIN_SLICE_MS. */ + int64_t wait_end = wait_ms < 0 ? INT64_MAX : ray_time_now_ms() + wait_ms; + int n = 0; + bool failed = false; + for (;;) { + bool std_ready = false; + for (uint32_t k = 0; k < nstd; k++) { + sevs[k] = stdin_events(poll->sels[sids[k]]->fd); + if (sevs[k]) std_ready = true; + } + + int remain = -1; + if (wait_end != INT64_MAX) { + int64_t r = wait_end - ray_time_now_ms(); + remain = r < 0 ? 0 : (r > INT_MAX ? INT_MAX : (int)r); + } + int slice = std_ready ? 0 : remain; + if (nstd && !std_ready && + (slice < 0 || slice > RAY_POLL_STDIN_SLICE_MS)) + slice = RAY_POLL_STDIN_SLICE_MS; + + if (nfds) { + n = WSAPoll(pfds, nfds, slice); + if (n == SOCKET_ERROR) { + if (WSAGetLastError() == WSAEINTR) continue; + failed = true; + break; + } + } else if (slice > 0) { + Sleep((DWORD)slice); + } else if (slice < 0) { + /* Only reachable with a live selector that is neither a + * socket nor stdin; nothing can wake us, so just yield. */ + Sleep(RAY_POLL_STDIN_SLICE_MS); + } + + if (n > 0 || std_ready) break; + if (remain == 0) break; /* deadline reached */ + if (!nstd && slice == remain) break; /* whole wait elapsed */ + } + + if (!failed) { + for (uint32_t j = 0; j < nfds && n > 0; j++) { + short re = pfds[j].revents; + if (!re) continue; + int ev = 0; + if (re & (POLLRDNORM | POLLRDBAND)) ev |= EV_IN; + if (re & POLLWRNORM) ev |= EV_OUT; + if (re & (POLLERR | POLLHUP | POLLNVAL)) ev |= EV_HUP; + /* The slot may have been reused by an earlier dispatch in + * this pass (deregister + accept): only act on the socket + * the event was reported for. */ + ray_selector_t* sel = pids[j] < poll->n_sels ? poll->sels[pids[j]] : NULL; + if (!sel || (SOCKET)sel->fd != pfds[j].fd) continue; + poll_dispatch(poll, pids[j], ev); + } + for (uint32_t k = 0; k < nstd; k++) { + if (!sevs[k]) continue; + ray_selector_t* sel = sids[k] < poll->n_sels ? poll->sels[sids[k]] : NULL; + if (!sel || sel->type != RAY_SEL_STDIN) continue; + poll_dispatch(poll, sids[k], sevs[k]); + } + } + + ray_sys_free(pfds); + ray_sys_free(pids); + ray_sys_free(sids); + ray_sys_free(sevs); + if (failed) return -1; + + if (poll->timers) { + if (ray_timers_fire_expired((ray_timers_t*)poll->timers) > 0) + ray_heap_note_activity(); + } + ray_heap_decay(); + if (bounded) break; + } + + return poll->code >= 0 ? poll->code : 0; } int64_t ray_poll_run(ray_poll_t* poll) @@ -72,4 +473,4 @@ int64_t ray_poll_run(ray_poll_t* poll) return ray_poll_run_for(poll, -1); } -#endif /* _WIN32 */ +#endif /* RAY_OS_WINDOWS */ diff --git a/src/core/ipc.c b/src/core/ipc.c index 5143b5ce6..c25935101 100644 --- a/src/core/ipc.c +++ b/src/core/ipc.c @@ -39,6 +39,7 @@ #ifdef RAY_OS_WINDOWS #define WIN32_LEAN_AND_MEAN + #include /* dup, dup2, close */ #include #include #else @@ -719,13 +720,31 @@ static ray_t* eval_payload_core(uint8_t* payload, size_t payload_len, * to fail the whole request because /tmp is full. The captured * string is then empty, and the response shape is still the 2-elem * list — clients can rely on that invariant. */ +#ifdef RAY_OS_WINDOWS +/* MSVCRT's tmpfile() creates its file in the drive root (admin-only on a + * stock system); use the user's temp directory, deleted on close ("D"). + * With no TMP/TEMP/USERPROFILE in the environment GetTempPath falls back + * to the (unwritable) Windows directory, so retry in the working dir. */ +static FILE* ipc_tmpfile(void) +{ + char dir[MAX_PATH + 1], path[MAX_PATH + 1]; + DWORD n = GetTempPathA(sizeof(dir), dir); + if (n == 0 || n > sizeof(dir) || !GetTempFileNameA(dir, "ray", 0, path)) { + if (!GetTempFileNameA(".", "ray", 0, path)) return NULL; + } + return fopen(path, "w+bD"); +} +#else +#define ipc_tmpfile() tmpfile() +#endif + static ray_t* eval_payload(uint8_t* payload, size_t payload_len, ray_ipc_header_t* hdr) { if (!(hdr->flags & RAY_IPC_FLAG_VERBOSE)) return eval_payload_core(payload, payload_len, hdr); - FILE* cap = tmpfile(); + FILE* cap = ipc_tmpfile(); int saved_out = -1, saved_err = -1; bool capturing = false; @@ -831,9 +850,13 @@ static int64_t ipc_send_fn(int64_t fd, uint8_t* buf, int64_t len) #ifdef RAY_OS_WINDOWS int n = send((ray_sock_t)fd, (const char*)buf, (int)len, 0); if (n < 0) { + /* Always overwrite errno: callers treat EAGAIN as "queue and retry", + * so a stale EAGAIN left over from an earlier call would turn a dead + * socket into a silently parked frame. */ int e = WSAGetLastError(); if (e == WSAEWOULDBLOCK) errno = EAGAIN; else if (e == WSAEINTR) errno = EINTR; + else errno = EIO; } return n; #else @@ -855,6 +878,7 @@ static int64_t ipc_send_fn(int64_t fd, uint8_t* buf, int64_t len) * ray_request_interrupt(). */ static volatile sig_atomic_t g_ipc_active_fd = -1; +#ifndef RAY_OS_WINDOWS /* no SIGURG on Windows: OOB cancel is POSIX-only */ static void ipc_sigurg_handler(int sig) { (void)sig; @@ -867,6 +891,7 @@ static void ipc_sigurg_handler(int sig) if (ray_sock_take_oob((ray_sock_t)fd)) ray_request_interrupt(); } +#endif static void ipc_install_oob_cancel(void) { diff --git a/src/core/platform.c b/src/core/platform.c index 89852918a..bad99c91d 100644 --- a/src/core/platform.c +++ b/src/core/platform.c @@ -469,6 +469,7 @@ void ray_sem_signal(ray_sem_t* s) { #define WIN32_LEAN_AND_MEAN #endif #include +#include /* _get_osfhandle */ #include "mem/sys.h" /* -------------------------------------------------------------------------- @@ -518,13 +519,34 @@ void* ray_vm_map_file(const char* path, size_t* out_size) { void ray_vm_unmap_file(void* ptr, size_t size) { if (!ptr) return; - UnmapViewOfFile(ptr); + /* munmap releases exactly [ptr, ptr+size) and is a no-op (EINVAL) for an + * unaligned ptr — the heap relies on that when it frees a block living + * inside a column's mapping (a passenger index). UnmapViewOfFile instead + * drops the WHOLE view containing ptr, which would pull the column out + * from under its live references. So only unmap at the view's own base; + * an interior block goes away with the view. The byte accounting still + * follows the call, exactly as on POSIX. */ + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(ptr, &mbi, sizeof(mbi)) && mbi.AllocationBase == ptr) + UnmapViewOfFile(ptr); ray_sys_track_file_sub((int64_t)size); } -/* Windows never reaches the fd/mmap CSV path (#ifndef RAY_OS_WINDOWS), so this - * is an unused stub kept only for API completeness. */ -void* ray_vm_map_fd_ro(int fd, size_t size) { (void)fd; (void)size; return NULL; } +/* Read-only view of an open CRT descriptor (the CSV reader maps the file this + * way, then closes the fd). The view keeps the file alive on its own, so both + * the mapping handle and the caller's fd may be closed afterwards. */ +void* ray_vm_map_fd_ro(int fd, size_t size) { + if (size == 0) return NULL; + HANDLE hFile = (HANDLE)_get_osfhandle(fd); + if (hFile == INVALID_HANDLE_VALUE) return NULL; + HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if (!hMap) return NULL; + void* p = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, size); + CloseHandle(hMap); + if (!p) return NULL; + ray_sys_track_file_add((int64_t)size); + return p; +} void ray_vm_advise_seq(void* ptr, size_t size) { /* PrefetchVirtualMemory is Win8.1+. Best-effort; ignore failure. */ @@ -546,16 +568,25 @@ void ray_vm_release_block(void* blk, size_t bsize, bool hugepage) { } void* ray_vm_alloc_aligned(size_t size, size_t alignment) { - /* Over-allocate, find aligned offset. Can't trim on Windows, so the - * pool header's vm_base field stores the original base for VirtualFree. */ - void* mem = VirtualAlloc(NULL, size + alignment, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - if (!mem) return NULL; - uintptr_t aligned = ((uintptr_t)mem + alignment - 1) & ~(alignment - 1); - /* Count the kept `size` to balance ray_vm_free(ptr, size); the alignment - * slack Windows cannot trim is left uncounted (parity with POSIX). */ - ray_sys_track_add((int64_t)size); - return (void*)aligned; + /* VirtualFree(MEM_RELEASE) only accepts the exact base VirtualAlloc + * returned, and a reservation cannot be trimmed. So find an aligned + * hole by reserving size+alignment, release it, and allocate exactly + * `size` at the aligned address inside it. Another thread may take the + * hole in between; retry a few times. The result is its own allocation + * base, so ray_vm_free(ptr, size) releases it like any other block. */ + for (int attempt = 0; attempt < 16; attempt++) { + void* probe = VirtualAlloc(NULL, size + alignment, MEM_RESERVE, PAGE_NOACCESS); + if (!probe) return NULL; + uintptr_t aligned = ((uintptr_t)probe + alignment - 1) & ~(alignment - 1); + VirtualFree(probe, 0, MEM_RELEASE); + void* p = VirtualAlloc((void*)aligned, size, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (p) { + ray_sys_track_add((int64_t)size); + return p; + } + } + return NULL; } bool ray_vm_hugepage(void* ptr, size_t size) { (void)ptr; (void)size; return false; } diff --git a/src/core/profile.h b/src/core/profile.h index 02a71ab6a..cc5ab6ad7 100644 --- a/src/core/profile.h +++ b/src/core/profile.h @@ -28,6 +28,9 @@ #include #if defined(RAY_OS_WINDOWS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN /* keep / macros out */ +#endif #include #else #include diff --git a/src/core/sock.c b/src/core/sock.c index 750af93a2..18f0c502c 100644 --- a/src/core/sock.c +++ b/src/core/sock.c @@ -21,7 +21,7 @@ * SOFTWARE. */ -#ifndef RAY_OS_WINDOWS +#ifndef _WIN32 #define _GNU_SOURCE #endif @@ -48,6 +48,44 @@ /* ===== Socket Implementation ===== */ +#ifdef RAY_OS_WINDOWS +/* Winsock must be initialised before the process's first socket call; do it + * at load time so no entry point (listen, connect, a bare client) can miss + * it. Never paired with WSACleanup: process exit releases it. */ +__attribute__((constructor)) static void sock_win_startup(void) +{ + WSADATA wsa; + (void)WSAStartup(MAKEWORD(2, 2), &wsa); +} + +/* Winsock reports failures through WSAGetLastError(), never errno. The + * callers (poll loop, IPC) branch on errno == EINTR / EAGAIN, so mirror the + * Winsock code into errno after every failed socket call (or a code taken + * from SO_ERROR). */ +static void sock_win_errno_code(int code) +{ + switch (code) { + case WSAEWOULDBLOCK: errno = EAGAIN; break; + case WSAEINTR: errno = EINTR; break; + case WSAEINPROGRESS: errno = EINPROGRESS; break; + case WSAETIMEDOUT: errno = ETIMEDOUT; break; + case WSAECONNRESET: errno = ECONNRESET; break; + case WSAECONNABORTED: errno = ECONNABORTED; break; + case WSAECONNREFUSED: errno = ECONNREFUSED; break; + case WSAENOTCONN: errno = ENOTCONN; break; + case WSAENOTSOCK: errno = ENOTSOCK; break; + case WSAEADDRINUSE: errno = EADDRINUSE; break; + case WSAEINVAL: errno = EINVAL; break; + default: errno = EIO; break; + } +} + +static void sock_win_errno(void) +{ + sock_win_errno_code(WSAGetLastError()); +} +#endif + ray_sock_t ray_sock_listen_at(const char* host, uint16_t port) { /* NULL/empty host keeps the historical INADDR_ANY bind. A host that @@ -68,7 +106,15 @@ ray_sock_t ray_sock_listen_at(const char* host, uint16_t port) if (fd == RAY_INVALID_SOCK) return RAY_INVALID_SOCK; int yes = 1; +#ifdef RAY_OS_WINDOWS + /* Windows SO_REUSEADDR lets a second socket bind a port another one is + * actively listening on (the bind "steals" it). The POSIX meaning — + * rebind right after a restart, but fail while the port is in use — is + * SO_EXCLUSIVEADDRUSE here (TIME_WAIT never blocks a Windows bind). */ + setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (const char*)&yes, sizeof(yes)); +#else setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes)); +#endif struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); @@ -77,10 +123,16 @@ ray_sock_t ray_sock_listen_at(const char* host, uint16_t port) addr.sin_port = htons(port); if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { +#ifdef RAY_OS_WINDOWS + sock_win_errno(); /* e.g. EADDRINUSE for a taken port */ +#endif ray_sock_close(fd); return RAY_INVALID_SOCK; } if (listen(fd, 128) < 0) { +#ifdef RAY_OS_WINDOWS + sock_win_errno(); +#endif ray_sock_close(fd); return RAY_INVALID_SOCK; } @@ -97,6 +149,9 @@ ray_sock_t ray_sock_accept(ray_sock_t srv) ray_sock_t fd; do { fd = (ray_sock_t)accept(srv, NULL, NULL); +#ifdef RAY_OS_WINDOWS + if (fd == RAY_INVALID_SOCK) sock_win_errno(); +#endif } while (fd == RAY_INVALID_SOCK && errno == EINTR); if (fd == RAY_INVALID_SOCK) return RAY_INVALID_SOCK; @@ -115,8 +170,13 @@ ray_sock_t ray_sock_accept(ray_sock_t srv) static int sock_connect_one(ray_sock_t fd, const struct sockaddr* addr, socklen_t addrlen, int timeout_ms) { - if (timeout_ms <= 0) - return connect(fd, addr, addrlen) < 0 ? -1 : 0; + if (timeout_ms <= 0) { + if (connect(fd, addr, addrlen) == 0) return 0; +#ifdef RAY_OS_WINDOWS + sock_win_errno(); /* callers tell refusal from other failures by errno */ +#endif + return -1; + } ray_sock_set_nonblocking(fd); int rc = connect(fd, addr, addrlen); @@ -124,6 +184,7 @@ static int sock_connect_one(ray_sock_t fd, const struct sockaddr* addr, #ifdef RAY_OS_WINDOWS int werr = WSAGetLastError(); int in_progress = (werr == WSAEWOULDBLOCK || werr == WSAEINPROGRESS); + if (!in_progress) sock_win_errno_code(werr); #else int in_progress = (errno == EINPROGRESS); #endif @@ -136,13 +197,22 @@ static int sock_connect_one(ray_sock_t fd, const struct sockaddr* addr, do { pr = poll(&pfd, 1, timeout_ms); } while (pr < 0 && errno == EINTR); #endif if (pr == 0) { errno = ETIMEDOUT; return -1; } - if (pr < 0) return -1; + if (pr < 0) { +#ifdef RAY_OS_WINDOWS + sock_win_errno(); +#endif + return -1; + } /* Writable: harvest the pending connect result via SO_ERROR. */ int soerr = 0; socklen_t soerr_len = sizeof(soerr); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char*)&soerr, &soerr_len) < 0 || soerr != 0) { +#ifdef RAY_OS_WINDOWS + if (soerr != 0) sock_win_errno_code(soerr); /* a WSAE* code */ +#else if (soerr != 0) errno = soerr; +#endif return -1; } } @@ -213,6 +283,7 @@ int64_t ray_sock_send(ray_sock_t s, const void* buf, size_t len) while (rem > 0) { #ifdef RAY_OS_WINDOWS int n = send(s, (const char*)p, (int)rem, 0); + if (n < 0) sock_win_errno(); #else ssize_t n = send(s, p, rem, MSG_NOSIGNAL); #endif @@ -221,7 +292,11 @@ int64_t ray_sock_send(ray_sock_t s, const void* buf, size_t len) if (errno == EAGAIN || errno == EWOULDBLOCK) { /* Wait for write-readiness before retry */ struct pollfd pfd = { .fd = s, .events = POLLOUT }; +#ifdef RAY_OS_WINDOWS + WSAPoll(&pfd, 1, -1); +#else poll(&pfd, 1, -1); +#endif continue; } return -1; @@ -237,6 +312,7 @@ int64_t ray_sock_recv(ray_sock_t s, void* buf, size_t len) for (;;) { #ifdef RAY_OS_WINDOWS int n = recv(s, (char*)buf, (int)len, 0); + if (n < 0) sock_win_errno(); #else ssize_t n = recv(s, buf, len, 0); #endif diff --git a/src/core/sock.h b/src/core/sock.h index e35e566c1..d400dbef6 100644 --- a/src/core/sock.h +++ b/src/core/sock.h @@ -24,7 +24,7 @@ #ifndef RAY_SOCK_H #define RAY_SOCK_H -#include +#include "core/platform.h" /* ===== Socket Abstraction ===== */ diff --git a/src/io/csv.c b/src/io/csv.c index 269fb3daa..25302c1f9 100644 --- a/src/io/csv.c +++ b/src/io/csv.c @@ -65,8 +65,8 @@ #include #ifndef RAY_OS_WINDOWS #include -#endif #include +#endif /* -------------------------------------------------------------------------- * Constants @@ -104,7 +104,9 @@ static inline uint64_t csv_prog_at(size_t file_size, unsigned pct) { * mmap flags * -------------------------------------------------------------------------- */ +#ifndef RAY_OS_WINDOWS #define MMAP_FLAGS MAP_PRIVATE +#endif /* -------------------------------------------------------------------------- * Scratch memory helpers (same pattern as exec.c). diff --git a/src/mem/heap.c b/src/mem/heap.c index 1f82a7e69..906ef225e 100644 --- a/src/mem/heap.c +++ b/src/mem/heap.c @@ -44,11 +44,21 @@ #include /* getpid, close, ftruncate, unlink */ #include /* open, fcntl, F_PREALLOCATE on macOS */ #include -#include /* mmap, munmap */ #include /* O_* modes */ #include #include +/* File-backed spill (a pool or direct block mapped over a preallocated temp + * file when the anonymous mapping is refused) is POSIX-only. Windows pools + * take only the anonymous VirtualAlloc path (docs/architecture/memory.md): + * the spill helpers are compiled out and swap_fd stays -1 there. */ +#if defined(RAY_OS_WINDOWS) +# define RAY_HEAP_FILE_SPILL 0 +#else +# define RAY_HEAP_FILE_SPILL 1 +# include /* mmap, munmap */ +#endif + #ifdef DEBUG /* ===================================================================== * Debug-only stale-pointer detector (issue #240 investigation). @@ -64,7 +74,9 @@ * choice for chasing double releases (found while debugging #240). * RAY_DFD_NO_ABORT=1 reports without aborting. * ===================================================================== */ +#if !defined(RAY_OS_WINDOWS) #include +#endif #define DFD_CAP_BITS 20 #define DFD_CAP (1u << DFD_CAP_BITS) @@ -149,10 +161,12 @@ static void dfd_purge_range(uintptr_t lo, uintptr_t hi) { } static void dfd_report(const char* who, const void* p) { + fprintf(stderr, "\n=== DFD: %s on FREED block %p ===\n", who, p); +#if !defined(RAY_OS_WINDOWS) void* frames[64]; int n = backtrace(frames, 64); - fprintf(stderr, "\n=== DFD: %s on FREED block %p ===\n", who, p); backtrace_symbols_fd(frames, n, 2); +#endif fflush(stderr); if (!getenv("RAY_DFD_NO_ABORT")) abort(); } @@ -179,6 +193,7 @@ static void dfd_validate_freelists(void); * contiguous first, fall back to non-contiguous, then ftruncate to * extend the file size if needed (F_PREALLOCATE doesn't grow the file * beyond its current size). */ +#if RAY_HEAP_FILE_SPILL static int heap_preallocate(int fd, off_t offset, off_t len) { #if defined(__APPLE__) fstore_t fs = { @@ -202,6 +217,7 @@ static int heap_preallocate(int fd, off_t offset, off_t len) { return posix_fallocate(fd, offset, len); #endif } +#endif /* RAY_HEAP_FILE_SPILL */ /* -------------------------------------------------------------------------- * Static asserts @@ -574,6 +590,9 @@ static bool heap_add_pool(ray_heap_t* h, uint8_t order) { if (!heap_anon_would_exceed(pool_size)) mem = ray_vm_alloc_aligned(pool_size, pool_size); +#if !RAY_HEAP_FILE_SPILL + if (!mem) return false; +#else if (!mem) { /* Anonymous mmap refused — usually means RAM+swap can't satisfy * pool_size right now. Fall back to file-backed mmap: create a @@ -668,6 +687,7 @@ static bool heap_add_pool(ray_heap_t* h, uint8_t order) { ray_sys_free(swap_path); swap_path = NULL; } +#endif /* RAY_HEAP_FILE_SPILL */ /* Enable transparent huge pages on anon pools (Linux). Self-aligned * 32MB pools are 2MB-aligned, hence THP-eligible. Never on file-backed @@ -1184,6 +1204,10 @@ static void ray_detach_owned_refs(ray_t* v) { * failure. */ static void* heap_direct_map_file(ray_heap_t* h, size_t map_size, int* out_fd, char** out_path) { +#if !RAY_HEAP_FILE_SPILL + (void)h; (void)map_size; (void)out_fd; (void)out_path; + return NULL; +#else static _Atomic uint64_t direct_swap_counter = 0; uint64_t cnt = atomic_fetch_add_explicit(&direct_swap_counter, 1, memory_order_relaxed); @@ -1213,6 +1237,7 @@ static void* heap_direct_map_file(ray_heap_t* h, size_t map_size, *out_fd = fd; *out_path = path; return mapped; +#endif /* RAY_HEAP_FILE_SPILL */ } /* -------------------------------------------------------------------------- @@ -1713,6 +1738,7 @@ void ray_free(ray_t* v) { if (h) RAY_STAT(h->stats.free_count++); atomic_fetch_sub_explicit(&g_direct_bytes, (int64_t)map_size, memory_order_relaxed); atomic_fetch_sub_explicit(&g_direct_count, 1, memory_order_relaxed); +#if RAY_HEAP_FILE_SPILL if (swap_fd >= 0) { /* File-backed spill: mapped directly (not via ray_vm_alloc), so * unmap + uncount by hand, then close and unlink the spill file. */ @@ -1720,7 +1746,11 @@ void ray_free(ray_t* v) { ray_sys_track_sub((int64_t)map_size); close(swap_fd); if (swap_path) { unlink(swap_path); ray_sys_free(swap_path); } - } else if (direct_cache_put(base, map_size)) { + } else +#else + (void)swap_fd; (void)swap_path; +#endif + if (direct_cache_put(base, map_size)) { /* Stashed for reuse: pages stay resident, so the block keeps * its committed-RAM and watermark accounting; only the live * stats above dropped. Eviction (direct_cache_drain) performs diff --git a/src/ops/builtins.c b/src/ops/builtins.c index 18dd8d35b..9b2fb31bf 100644 --- a/src/ops/builtins.c +++ b/src/ops/builtins.c @@ -110,7 +110,7 @@ void ray_lang_print(FILE* fp, ray_t* val) { return; } switch (val->type) { - case -RAY_I64: fprintf(fp, "%ld", (long)val->i64); break; + case -RAY_I64: fprintf(fp, "%" PRId64, val->i64); break; case -RAY_F64: { double fv = val->f64; fv = clear_neg_zero(fv); @@ -142,8 +142,8 @@ void ray_lang_print(FILE* fp, ray_t* val) { break; } case RAY_TABLE: - fprintf(fp, "", - (long)ray_table_nrows(val), (long)ray_table_ncols(val)); + fprintf(fp, "
", + ray_table_nrows(val), ray_table_ncols(val)); break; case RAY_UNARY: case RAY_BINARY: case RAY_VARY: { const char* name = ray_fn_name(val); @@ -204,7 +204,7 @@ static char* fmt_interpolate(const char* fmt, size_t flen, ray_t** args, int64_t RAY_ATOM_IS_NULL(a)) { tlen = snprintf(tmp, sizeof(tmp), "%s", null_literal_str(a->type)); } else if (a->type == -RAY_I64) { - tlen = snprintf(tmp, sizeof(tmp), "%ld", (long)a->i64); + tlen = snprintf(tmp, sizeof(tmp), "%" PRId64, a->i64); } else if (a->type == -RAY_F64) { double fv = a->f64; fv = clear_neg_zero(fv); diff --git a/src/ops/pivot.c b/src/ops/pivot.c index f3b6eb52c..0be4a5579 100644 --- a/src/ops/pivot.c +++ b/src/ops/pivot.c @@ -21,6 +21,7 @@ * SOFTWARE. */ +#include #include "ops/internal.h" #include "ops/hash.h" #include "ops/idxop.h" @@ -1910,9 +1911,9 @@ ray_t* exec_pivot(ray_graph_t* g, ray_op_t* op, ray_t* tbl) { len = snprintf(buf, sizeof(buf), "%s", pval ? "true" : "false"); } else if (pt == RAY_I64 || pt == RAY_I32 || pt == RAY_I16 || pt == RAY_DATE || pt == RAY_TIME || pt == RAY_TIMESTAMP) { - len = snprintf(buf, sizeof(buf), "%ld", (long)pval); + len = snprintf(buf, sizeof(buf), "%" PRId64, pval); } else { - len = snprintf(buf, sizeof(buf), "col%ld", (long)pval); + len = snprintf(buf, sizeof(buf), "col%" PRId64, pval); } col_sym = ray_sym_intern(buf, (size_t)len); } diff --git a/src/ops/query.c b/src/ops/query.c index 2008af99a..056d23882 100644 --- a/src/ops/query.c +++ b/src/ops/query.c @@ -11017,7 +11017,9 @@ ray_t* ray_select(ray_t** args, int64_t n) { /* Type-aware key element reader. Normalizes any * comparable scalar key into an int64_t so linear * scans can use equality. For floats we bitcast so - * NaN and -0/+0 match the DAG's hash-equality. */ + * NaN and -0/+0 match the DAG's hash-equality. + * (via ) owns the name on Windows. */ + #undef KEY_READ #define KEY_READ(dst, vec, base_type, idx) do { \ const void* _d = ray_data(vec); \ switch (base_type) { \ diff --git a/src/ops/system.c b/src/ops/system.c index 176415645..6d5a55cce 100644 --- a/src/ops/system.c +++ b/src/ops/system.c @@ -65,6 +65,7 @@ void* ray_runtime_get_sys_args(void); #define RAY_POPEN(c, m) popen((c), (m)) #define RAY_PCLOSE(f) pclose(f) #else +#include /* access, F_OK */ #define RAY_POPEN(c, m) _popen((c), (m)) #define RAY_PCLOSE(f) _pclose(f) #endif @@ -1532,10 +1533,23 @@ ray_t* ray_sysinfo_fn(ray_t** args, int64_t n) { ray_t* v3 = make_i64(ray_sys_total_ram()); vals = ray_list_append(vals, v3); ray_release(v3); #else + SYSTEM_INFO si; + GetSystemInfo(&si); + int64_t s1 = ray_sym_intern("cores", 5); keys = ray_vec_append(keys, &s1); - ray_t* v1 = make_i64(1); + ray_t* v1 = make_i64((int64_t)si.dwNumberOfProcessors); vals = ray_list_append(vals, v1); ray_release(v1); + + int64_t s2 = ray_sym_intern("page-size", 9); + keys = ray_vec_append(keys, &s2); + ray_t* v2 = make_i64((int64_t)si.dwPageSize); + vals = ray_list_append(vals, v2); ray_release(v2); + + int64_t s3 = ray_sym_intern("total-mem", 9); + keys = ray_vec_append(keys, &s3); + ray_t* v3 = make_i64(ray_sys_total_ram()); + vals = ray_list_append(vals, v3); ray_release(v3); #endif /* Process and host identity (#573). An embedded process previously had diff --git a/src/ops/tblop.c b/src/ops/tblop.c index 7da33443a..375d4ae3b 100644 --- a/src/ops/tblop.c +++ b/src/ops/tblop.c @@ -627,7 +627,7 @@ static ray_t* pivot_fn_impl(ray_t* tbl, ray_t* index_arg, ray_t* pivot_col_name, if (pval->type == -RAY_SYM) { col_sym = pval->i64; } else if (pval->type == -RAY_I64) { - char buf[64]; int len = snprintf(buf, sizeof(buf), "%ld", (long)pval->i64); + char buf[64]; int len = snprintf(buf, sizeof(buf), "%" PRId64, pval->i64); col_sym = ray_sym_intern(buf, (size_t)len); } else if (pval->type == -RAY_F64) { double fv = clear_neg_zero(pval->f64); @@ -636,7 +636,7 @@ static ray_t* pivot_fn_impl(ray_t* tbl, ray_t* index_arg, ray_t* pivot_col_name, } else if (pval->type == -RAY_BOOL) { col_sym = ray_sym_intern(pval->b8 ? "true" : "false", pval->b8 ? 4 : 5); } else { - char buf[64]; int len = snprintf(buf, sizeof(buf), "col%ld", (long)pval->i64); + char buf[64]; int len = snprintf(buf, sizeof(buf), "col%" PRId64, pval->i64); col_sym = ray_sym_intern(buf, (size_t)len); } if (a1) ray_release(pval); diff --git a/src/store/aof.c b/src/store/aof.c index fe01b480b..e85831ae6 100644 --- a/src/store/aof.c +++ b/src/store/aof.c @@ -49,6 +49,15 @@ #include #include +/* ray_file_sync takes the platform handle: the descriptor itself on POSIX, + * the underlying HANDLE on Windows. */ +#ifdef RAY_OS_WINDOWS +#include +#define AOF_FP_HANDLE(fp) ((ray_fd_t)_get_osfhandle(fileno(fp))) +#else +#define AOF_FP_HANDLE(fp) ((ray_fd_t)fileno(fp)) +#endif + #define AOF_PATH_MAX 1024 /* Segment-path buffers are sized past the worst case (dir + '/' + 24-char * segment name) so gcc's -Wformat-truncation can prove snprintf fits even @@ -390,7 +399,7 @@ static ray_err_t aof_rotate(ray_aof_t* log) { if (err != RAY_OK) return err; } if (fflush(log->fp) != 0) return RAY_ERR_IO; - if (ray_file_sync((ray_fd_t)fileno(log->fp)) != RAY_OK) return RAY_ERR_IO; + if (ray_file_sync(AOF_FP_HANDLE(log->fp)) != RAY_OK) return RAY_ERR_IO; if (fclose(log->fp) != 0) { log->fp = NULL; return RAY_ERR_IO; } char path[AOF_SEGPATH_MAX]; @@ -442,7 +451,7 @@ ray_err_t ray_aof_commit(ray_aof_t* log) { ray_err_t err = aof_write_frame(log); if (err != RAY_OK) return err; if (fflush(log->fp) != 0) return RAY_ERR_IO; - return ray_file_sync((ray_fd_t)fileno(log->fp)); + return ray_file_sync(AOF_FP_HANDLE(log->fp)); } int64_t ray_aof_next_lsn(const ray_aof_t* log) { diff --git a/src/store/csr.c b/src/store/csr.c index 59f43b5ae..b042fb51b 100644 --- a/src/store/csr.c +++ b/src/store/csr.c @@ -24,6 +24,7 @@ #include "csr.h" #include "store/col.h" #include "mem/sys.h" +#include "store/fileio.h" /* ray_mkdir */ #include #include #include @@ -464,7 +465,7 @@ ray_err_t ray_rel_save(ray_rel_t* rel, const char* dir) { if (!rel || !dir) return RAY_ERR_IO; /* Create directory */ - if (mkdir(dir, 0755) != 0 && errno != EEXIST) return RAY_ERR_IO; + if (ray_mkdir(dir) != RAY_OK) return RAY_ERR_IO; ray_err_t err = csr_save(&rel->fwd, dir, "fwd"); if (err != RAY_OK) return err; diff --git a/src/store/fileio.h b/src/store/fileio.h index 658e56062..c6f954c82 100644 --- a/src/store/fileio.h +++ b/src/store/fileio.h @@ -24,10 +24,13 @@ #ifndef RAY_FILEIO_H #define RAY_FILEIO_H -#include +#include "core/platform.h" /* Cross-platform file I/O (locking, sync, atomic rename) */ #ifdef RAY_OS_WINDOWS + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN /* keep / macros out */ + #endif #include typedef HANDLE ray_fd_t; #define RAY_FD_INVALID INVALID_HANDLE_VALUE diff --git a/src/store/hnsw.c b/src/store/hnsw.c index 339119a19..a350c4335 100644 --- a/src/store/hnsw.c +++ b/src/store/hnsw.c @@ -23,6 +23,7 @@ #include "hnsw.h" #include "mem/sys.h" +#include "store/fileio.h" /* ray_mkdir */ #include #include #include @@ -818,7 +819,7 @@ typedef struct { ray_err_t ray_hnsw_save(const ray_hnsw_t* idx, const char* dir) { if (!idx || !dir) return RAY_ERR_IO; - if (mkdir(dir, 0755) != 0 && errno != EEXIST) return RAY_ERR_IO; + if (ray_mkdir(dir) != RAY_OK) return RAY_ERR_IO; char path[1024]; FILE* f; diff --git a/src/table/domain.c b/src/table/domain.c index ba87c492d..0e9f58697 100644 --- a/src/table/domain.c +++ b/src/table/domain.c @@ -210,6 +210,40 @@ static inline void dom_unlock(void) { /* ---- FILE domain construction / destruction ------------------------------- */ +/* realpath(3) contract: absolute path of an EXISTING file, else NULL. + * Windows has no realpath; _fullpath only normalizes (it succeeds for + * missing files too), so existence is checked separately. */ +static char* dom_realpath(const char* path, char resolved[PATH_MAX]) { +#if defined(RAY_OS_WINDOWS) + if (!_fullpath(resolved, path, PATH_MAX)) return NULL; + if (GetFileAttributesA(resolved) == INVALID_FILE_ATTRIBUTES) return NULL; + return resolved; +#else + return realpath(path, resolved); +#endif +} + +/* Do two resolved paths name the same file? On Windows one file is + * reachable as C:\db\sym, C:\db/sym or C:\DB\Sym (NTFS is case-insensitive + * and _fullpath keeps the caller's separators and case), so compare with + * '\\' == '/' and ASCII case folded — a plain strcmp would key one symfile + * twice and open two diverging domains for it. The paths themselves are + * left as given: they are also used to create the file. */ +static bool dom_path_eq(const char* a, const char* b) { +#if defined(RAY_OS_WINDOWS) + for (;; a++, b++) { + char ca = *a == '\\' ? '/' : *a; + char cb = *b == '\\' ? '/' : *b; + if (ca >= 'A' && ca <= 'Z') ca = (char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (char)(cb - 'A' + 'a'); + if (ca != cb) return false; + if (!ca) return true; + } +#else + return strcmp(a, b) == 0; +#endif +} + /* Resolved cache key for `path`. realpath of the file when it exists; * for to-be-created symfiles, realpath of the parent + "/" + basename * (the parent must exist). malloc'd. */ @@ -218,7 +252,7 @@ static char* dom_resolve_path(const char* path) { * realpath(NULL)/strdup's libc-malloc'd buffers, so the returned key is * uniformly buddy-allocated and the caller releases it with ray_free_raw. */ char resolved[PATH_MAX]; - if (realpath(path, resolved)) { + if (dom_realpath(path, resolved)) { size_t n = strlen(resolved); char* out = (char*)ray_sys_alloc(n + 1); if (out) memcpy(out, resolved, n + 1); @@ -238,7 +272,7 @@ static char* dom_resolve_path(const char* path) { memcpy(tmp, path, plen + 1); char* dir = dirname(tmp); char rdir[PATH_MAX]; - if (!realpath(dir, rdir)) return NULL; + if (!dom_realpath(dir, rdir)) return NULL; size_t dlen = strlen(rdir); char* out = (char*)ray_sys_alloc(dlen + 1 + blen + 1); if (!out) return NULL; @@ -519,7 +553,7 @@ static ray_sym_domain_t* dom_open_impl(const char* path, bool create) { dom_lock(); for (ray_sym_domain_t* d = g_domains; d; d = d->next) { - if (strcmp(d->path, rpath) == 0) { + if (dom_path_eq(d->path, rpath)) { /* Revalidate: external append-only growth extends in place; * any other divergence is loud (NULL). */ size_t cur_size = exists ? (size_t)st.st_size : 0; @@ -591,7 +625,7 @@ static ray_sym_domain_t* dom_open_impl(const char* path, bool create) { * the winner (pointer equality must hold for one resolved path). */ dom_lock(); for (ray_sym_domain_t* e = g_domains; e; e = e->next) { - if (strcmp(e->path, d->path) == 0) { + if (dom_path_eq(e->path, d->path)) { e->rc++; dom_unlock(); dom_destroy(d); diff --git a/test/ipc_harness.h b/test/ipc_harness.h index 5f9066dd3..8dad45aea 100644 --- a/test/ipc_harness.h +++ b/test/ipc_harness.h @@ -48,8 +48,13 @@ #include "core/runtime.h" #include "mem/sys.h" #include "lang/internal.h" +#ifdef RAY_OS_WINDOWS +#include +#include +#else #include #include +#endif #include #include diff --git a/test/main.c b/test/main.c index 6cf59894f..42426e6a0 100644 --- a/test/main.c +++ b/test/main.c @@ -50,6 +50,7 @@ #include "lang/format.h" #include "ops/internal.h" #include "ops/idxop.h" +#include "store/fileio.h" /* ray_mkdir_p — ray_test_mkdir_p */ /* __RUNTIME is internal test plumbing; runtime API declarations come from * . */ @@ -376,6 +377,16 @@ static test_result_t run_rfl_file(const char* path) { src[r] = '\0'; fclose(f); + /* ";; @requires: posix" anywhere in a file marks it as depending on a + * POSIX shell / filesystem (.sys.exec pipelines, /proc). Where that + * does not exist the file is reported as SKIP, never silently dropped. */ +#if defined(_WIN32) + if (strstr(src, ";; @requires: posix")) { + free(src); + SKIP("requires POSIX shell/filesystem"); + } +#endif + int line_no = 0; int assert_count = 0; /* tallies LHS -- RHS and EXPR !- SUBSTR lines */ char* p = src; @@ -848,7 +859,58 @@ static int name_matches_filter(const char* name, const char* filter) { return strstr(name, filter) != NULL; } +/* ---- Shell-free filesystem helpers (declared in test.h) ---- */ + +int ray_test_rm_rf(const char* path) { + struct stat st; + if (lstat(path, &st) != 0) return 0; /* already gone */ + if (S_ISDIR(st.st_mode)) { + DIR* d = opendir(path); + if (d) { + struct dirent* ent; + char child[4096]; + while ((ent = readdir(d)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + continue; + snprintf(child, sizeof(child), "%s/%s", path, ent->d_name); + ray_test_rm_rf(child); + } + closedir(d); + } + return rmdir(path); + } + return unlink(path); +} + +int ray_test_mkdir_p(const char* path) { + return ray_mkdir_p(path) == RAY_OK ? 0 : -1; +} + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +long ray_test_sysconf(int name) { + SYSTEM_INFO si; + GetSystemInfo(&si); + switch (name) { + case _SC_PAGESIZE: return (long)si.dwPageSize; + case _SC_NPROCESSORS_ONLN: return (long)si.dwNumberOfProcessors; + case _SC_PHYS_PAGES: { + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) return -1; + return (long)(ms.ullTotalPhys / si.dwPageSize); + } + default: errno = EINVAL; return -1; + } +} +#endif + int main(int argc, char** argv) { +#if defined(_WIN32) + (void)_mkdir("/tmp"); /* tests use "/tmp/..." paths (see test.h) */ +#endif ray_expr_stats_init(); ray_idx_stats_init(); g_color = isatty(fileno(stdout)); diff --git a/test/rfl/collection/atomic_map_coverage.rfl b/test/rfl/collection/atomic_map_coverage.rfl index 7eba0de75..28c7e7b27 100644 --- a/test/rfl/collection/atomic_map_coverage.rfl +++ b/test/rfl/collection/atomic_map_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; atomic_map_binary_op / atomic_map_unary coverage. ;; Exercises boxed-list paths, empty collections, nested auto-map, ;; recursive map, and error propagation. diff --git a/test/rfl/collection/collection_branch_cov.rfl b/test/rfl/collection/collection_branch_cov.rfl index b187b9e05..dc0e28bb8 100644 --- a/test/rfl/collection/collection_branch_cov.rfl +++ b/test/rfl/collection/collection_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; collection_branch_cov.rfl — branch coverage for src/ops/collection.c ;; ;; Targets uncovered branches identified at 64.84% baseline. Each section diff --git a/test/rfl/collection/cov3.rfl b/test/rfl/collection/cov3.rfl index 0c2e08f59..dc23bea6d 100644 --- a/test/rfl/collection/cov3.rfl +++ b/test/rfl/collection/cov3.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov3.rfl — additional targeted coverage for collection.c remaining gaps ;; Focuses on: atom_eq LIST path, propagate_sym_dict, list_to_typed_vec empty SYM/STR, ;; take STR range out-of-bounds, take dict with typed vals, diff --git a/test/rfl/collection/cov4.rfl b/test/rfl/collection/cov4.rfl index 61b0a9bb0..07923cbf1 100644 --- a/test/rfl/collection/cov4.rfl +++ b/test/rfl/collection/cov4.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov4 — targeted coverage for collection.c remaining gaps ;; Focuses on: atom_eq different-length vecs, range-take type errors, ;; STR typed vec from CSV, STR range-take out-of-bounds, diff --git a/test/rfl/collection/cov5.rfl b/test/rfl/collection/cov5.rfl index 0dd92350a..2be5c80b5 100644 --- a/test/rfl/collection/cov5.rfl +++ b/test/rfl/collection/cov5.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov5 — targeted coverage: distinct_sort_cmp default branch (lines 282-291) ;; ;; F32 (type=6) is not in hs_hash_row switch → hashes by index (all "distinct"). diff --git a/test/rfl/collection/cov6.rfl b/test/rfl/collection/cov6.rfl index d6517d320..efd9d21bb 100644 --- a/test/rfl/collection/cov6.rfl +++ b/test/rfl/collection/cov6.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov6 — targeted coverage: parted_to_flat_vec STR path (lines 778-790) ;; ;; parted_to_flat_vec has two branches: diff --git a/test/rfl/group/group_key_types.rfl b/test/rfl/group/group_key_types.rfl index 8c868fe24..aca43f3fc 100644 --- a/test/rfl/group/group_key_types.rfl +++ b/test/rfl/group/group_key_types.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for group.c — key type diversity paths ;; ;; Targets: diff --git a/test/rfl/io/csv_branch_cov.rfl b/test/rfl/io/csv_branch_cov.rfl index 562e0e214..93eead2d6 100644 --- a/test/rfl/io/csv_branch_cov.rfl +++ b/test/rfl/io/csv_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Branch coverage for src/io/csv.c — targets reachable functional branches ;; left uncovered by csv_types.rfl, csv_round2.rfl, csv_splayed.rfl, ;; system/{read,write}_csv.rfl, and test/test_csv.c. diff --git a/test/rfl/io/csv_parallel_scan.rfl b/test/rfl/io/csv_parallel_scan.rfl index d8b0e7370..470235d49 100644 --- a/test/rfl/io/csv_parallel_scan.rfl +++ b/test/rfl/io/csv_parallel_scan.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Parallel row-offset scan (csv.c build_row_offsets_par) — exactness. ;; ;; The scan splits the file into chunks and reconciles quote parity across diff --git a/test/rfl/io/csv_rayfall_temporal.rfl b/test/rfl/io/csv_rayfall_temporal.rfl index 5163c1923..6f18420cd 100644 --- a/test/rfl/io/csv_rayfall_temporal.rfl +++ b/test/rfl/io/csv_rayfall_temporal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; CSV type inference must recognize Rayfall's display temporal forms. ;; These are the forms users see when DATE/TIMESTAMP values are printed. (.sys.exec "printf 'd,ts\n2024.01.02,2024.01.02D01:02:03.004005006\n' > rf_test_csv_rayfall_temporal.csv") -- 0 diff --git a/test/rfl/io/csv_types.rfl b/test/rfl/io/csv_types.rfl index 671f73668..f2a63358a 100644 --- a/test/rfl/io/csv_types.rfl +++ b/test/rfl/io/csv_types.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for src/io/csv.c — type inference, edge cases, parted writer. ;; ;; Targets (by approximate line number): diff --git a/test/rfl/io/read_until_eof.rfl b/test/rfl/io/read_until_eof.rfl index a4ea55ab6..2fe117c3f 100644 --- a/test/rfl/io/read_until_eof.rfl +++ b/test/rfl/io/read_until_eof.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; `read` / `read-bytes` must read until EOF, not to the size the file ;; reports — issue #572. ;; diff --git a/test/rfl/journal/ops_journal.rfl b/test/rfl/journal/ops_journal.rfl index b962325c3..0c04bd193 100644 --- a/test/rfl/journal/ops_journal.rfl +++ b/test/rfl/journal/ops_journal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage extension for src/ops/journal.c. ;; ;; The bulk of src/ops/journal.c is exercised by diff --git a/test/rfl/journal/ops_journal_purge.rfl b/test/rfl/journal/ops_journal_purge.rfl index e24369c1c..f21da7139 100644 --- a/test/rfl/journal/ops_journal_purge.rfl +++ b/test/rfl/journal/ops_journal_purge.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for .log.purge (src/ops/journal.c ray_log_purge_fn -> ;; src/store/journal.c ray_journal_purge) — issue #279. ;; diff --git a/test/rfl/lang/guid_entropy.rfl b/test/rfl/lang/guid_entropy.rfl index b77ceadbe..6d4e5511d 100644 --- a/test/rfl/lang/guid_entropy.rfl +++ b/test/rfl/lang/guid_entropy.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; `guid` must be unique across processes — issue #571. ;; ;; The generator seeded its per-thread xorshift state from rand(), and diff --git a/test/rfl/lang/parse_branch_cov.rfl b/test/rfl/lang/parse_branch_cov.rfl index 046871d53..44ca9bf04 100644 --- a/test/rfl/lang/parse_branch_cov.rfl +++ b/test/rfl/lang/parse_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Branch-coverage extension for src/lang/parse.c — the Rayfall parser ;; (tokenizer, atom-literal parsing, list/dict/vector syntax, comments, ;; escape sequences, error recovery). diff --git a/test/rfl/linkop/coverage.rfl b/test/rfl/linkop/coverage.rfl index 6aebc2bb5..6bba5bea8 100644 --- a/test/rfl/linkop/coverage.rfl +++ b/test/rfl/linkop/coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage workout for src/ops/linkop.c ;; Targets the regions NOT exercised by test/test_link.c: ;; - ray_col_link_fn error paths (lines 291, 293) diff --git a/test/rfl/null/slice_has_nulls.rfl b/test/rfl/null/slice_has_nulls.rfl index c30a383ec..60e0e9808 100644 --- a/test/rfl/null/slice_has_nulls.rfl +++ b/test/rfl/null/slice_has_nulls.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; slice_has_nulls.rfl — a slice sees its parent's nulls, in memory and on ;; disk (#495). ;; diff --git a/test/rfl/null/sort_null_placement.rfl b/test/rfl/null/sort_null_placement.rfl index 7f11b4a47..6a71ba30b 100644 --- a/test/rfl/null/sort_null_placement.rfl +++ b/test/rfl/null/sort_null_placement.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Where a null lands in a sort, pinned for every path that can run. ;; ;; A null is the SMALLEST value: ascending puts nulls first, descending last. diff --git a/test/rfl/null/sym_str_null_compare.rfl b/test/rfl/null/sym_str_null_compare.rfl index 40a13b4fc..bde1fa4ef 100644 --- a/test/rfl/null/sym_str_null_compare.rfl +++ b/test/rfl/null/sym_str_null_compare.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Equality comparison on NULLABLE SYM / STR columns — fused path vs unfused. ;; ;; A SYM null is sym id 0 and a STR null is a zero-length descriptor: both are diff --git a/test/rfl/ops/internal_coverage.rfl b/test/rfl/ops/internal_coverage.rfl index 15633da0c..a033b33a6 100644 --- a/test/rfl/ops/internal_coverage.rfl +++ b/test/rfl/ops/internal_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for src/ops/internal.h static-inline helpers that are ;; instantiated in production TUs (exec.c, filter.c, expr.c, etc.) ;; but have never been exercised through the test suite. diff --git a/test/rfl/query/update_by_sym_width.rfl b/test/rfl/query/update_by_sym_width.rfl index dc52d0227..c9b3d38a9 100644 --- a/test/rfl/query/update_by_sym_width.rfl +++ b/test/rfl/query/update_by_sym_width.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Regression: `update ... by: ` on a narrow-width SYM key column. ;; ;; SYM columns use an adaptive dictionary-index width (W8/W16/W32/W64 in attrs, diff --git a/test/rfl/regress/i64_text_width.rfl b/test/rfl/regress/i64_text_width.rfl new file mode 100644 index 000000000..cde583f7e --- /dev/null +++ b/test/rfl/regress/i64_text_width.rfl @@ -0,0 +1,17 @@ +;; An i64 must survive every text path in full 64-bit width. The +;; interpolation, print and pivot/column-name helpers used to render it with +;; "%ld" + (long), which is 32-bit on Windows (LLP64): 10^18 came back as +;; -1486618624, and a pivot keyed on large integers produced wrong column +;; names — a data defect, not just a display one. + +(format "%" 1000000000000000000) -- "1000000000000000000" +(format "%" 9223372036854775807) -- "9223372036854775807" +(format "%" -9223372036854775806) -- "-9223372036854775806" +(format "%,%" 4294967296 4294967297) -- "4294967296,4294967297" + +;; as 'STR shares none of that code — pin it as the independent oracle. +(== (format "%" 1099511627776) (as 'STR 1099511627776)) -- true + +;; pivot builds column names from the key values +(set _t64 (table [g k v] (list ['a 'a 'b] [4294967296 4294967297 4294967296] [1.0 2.0 3.0]))) +(cols (pivot _t64 'g 'k 'v sum)) -- ['g '4294967296 '4294967297] diff --git a/test/rfl/regress/load_select_isnull_mask.rfl b/test/rfl/regress/load_select_isnull_mask.rfl index d0fffb148..db46bbd4b 100644 --- a/test/rfl/regress/load_select_isnull_mask.rfl +++ b/test/rfl/regress/load_select_isnull_mask.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Regression: load → select ISNULL WHERE-mask corruption. ;; ;; exec_elementwise_unary once had dedicated ISNULL kernels for only some diff --git a/test/rfl/sort/sort_coverage2.rfl b/test/rfl/sort/sort_coverage2.rfl index 3d1dec16f..e0b34c284 100644 --- a/test/rfl/sort/sort_coverage2.rfl +++ b/test/rfl/sort/sort_coverage2.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Pass-7 additional sort.c coverage. ;; ;; Targets uncovered regions NOT hit by sort_coverage.rfl: diff --git a/test/rfl/storage/shared_sym_domain.rfl b/test/rfl/storage/shared_sym_domain.rfl index 119d49012..780677f79 100644 --- a/test/rfl/storage/shared_sym_domain.rfl +++ b/test/rfl/storage/shared_sym_domain.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; The client layout, end-to-end through the language surface (sym-domain ;; architecture, Task 7b): a parted `hist` plus a splayed `live` sharing ;; ONE symfile (root/.sym) — write, read, query, join (same-domain fast diff --git a/test/rfl/storage/splay_coverage.rfl b/test/rfl/storage/splay_coverage.rfl index ffc09d7f3..51b1be34d 100644 --- a/test/rfl/storage/splay_coverage.rfl +++ b/test/rfl/storage/splay_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage extension for src/store/splay.c. ;; ;; src/store/splay.c is exercised by: diff --git a/test/rfl/store/col_format_generation.rfl b/test/rfl/store/col_format_generation.rfl index a5d3cccc0..d9b2de411 100644 --- a/test/rfl/store/col_format_generation.rfl +++ b/test/rfl/store/col_format_generation.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; On-disk column format generation (the header `order` byte, offset 17). ;; ;; Regression: an engine build briefly stamped the generation to 1 and demanded diff --git a/test/rfl/store/indexed_col_aux_refs.rfl b/test/rfl/store/indexed_col_aux_refs.rfl index 962539ea1..0d2731be5 100644 --- a/test/rfl/store/indexed_col_aux_refs.rfl +++ b/test/rfl/store/indexed_col_aux_refs.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; An indexed column still owns what sits at aux bytes 8-15. ;; ;; A SYM column keeps its resolution domain there, a STR column its pool. diff --git a/test/rfl/strop/like_seen_proj.rfl b/test/rfl/strop/like_seen_proj.rfl index 84aa9ec2d..5e78351f9 100644 --- a/test/rfl/strop/like_seen_proj.rfl +++ b/test/rfl/strop/like_seen_proj.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Targeted parallel SYM-LIKE coverage for like_seen_fn / like_proj_fn. ;; ;; Both kernels are the worker bodies dispatched by ray_pool_dispatch diff --git a/test/rfl/strop/string_branch_cov.rfl b/test/rfl/strop/string_branch_cov.rfl index 24838bfb4..27b26bb8b 100644 --- a/test/rfl/strop/string_branch_cov.rfl +++ b/test/rfl/strop/string_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; string_branch_cov.rfl -- targeted branch coverage for src/ops/string.c ;; ;; Baseline: 61.41% (345 uncovered branches). This file targets the diff --git a/test/rfl/symbol/sym_coverage.rfl b/test/rfl/symbol/sym_coverage.rfl index 1b9e2b4af..01a23e7dc 100644 --- a/test/rfl/symbol/sym_coverage.rfl +++ b/test/rfl/symbol/sym_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage extension for src/table/sym.c. ;; ;; src/table/sym.c is exercised by many existing tests via CSV/splayed I/O. diff --git a/test/rfl/system/cli_flag_values.rfl b/test/rfl/system/cli_flag_values.rfl index c014eadac..2eb4e7039 100644 --- a/test/rfl/system/cli_flag_values.rfl +++ b/test/rfl/system/cli_flag_values.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cli_flag_values.rfl — a value-taking flag must not swallow the next flag, ;; and an unknown option must not be mistaken for the script (#600). ;; diff --git a/test/rfl/system/csv_auto_int_width.rfl b/test/rfl/system/csv_auto_int_width.rfl index fd8ff1192..91c37ea77 100644 --- a/test/rfl/system/csv_auto_int_width.rfl +++ b/test/rfl/system/csv_auto_int_width.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; `INT` schema token → auto narrowest integer width on .csv.splayed. ;; ;; A column declared `INT` in a .csv.splayed schema is parsed as int64, diff --git a/test/rfl/system/csv_explicit_numeric_types.rfl b/test/rfl/system/csv_explicit_numeric_types.rfl index d053c5209..facf89284 100644 --- a/test/rfl/system/csv_explicit_numeric_types.rfl +++ b/test/rfl/system/csv_explicit_numeric_types.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Explicit numeric schemas must survive the streaming .csv.splayed writer ;; and the mmap-backed .db.splayed.get reload without widening. diff --git a/test/rfl/system/db_get.rfl b/test/rfl/system/db_get.rfl index cb03cd4ae..66f6cf336 100644 --- a/test/rfl/system/db_get.rfl +++ b/test/rfl/system/db_get.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Multi-table roots through the explicit .db trio — the surface left ;; after the .db.*.mount removal: every table is opened by name with ;; .db.splayed.get / .db.parted.get; nothing is discovered or bound diff --git a/test/rfl/system/db_parted_fill.rfl b/test/rfl/system/db_parted_fill.rfl index 2574b271a..b0e3a947a 100644 --- a/test/rfl/system/db_parted_fill.rfl +++ b/test/rfl/system/db_parted_fill.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; .db.parted.fill — fill missing tables across a parted db's partitions. ;; ;; For every table present in ANY partition, write an empty copy (schema diff --git a/test/rfl/system/db_sym_resolution.rfl b/test/rfl/system/db_sym_resolution.rfl index daed3ae87..56fce7170 100644 --- a/test/rfl/system/db_sym_resolution.rfl +++ b/test/rfl/system/db_sym_resolution.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; SYM-domain symfile resolution precedence, end-to-end (sym-domain ;; architecture spec, "Surface"): ;; explicit argument | dir/.sym | partition-shaped parent -> root/.sym diff --git a/test/rfl/system/ipc_diff.rfl b/test/rfl/system/ipc_diff.rfl index e02f1f08f..31f627bd5 100644 --- a/test/rfl/system/ipc_diff.rfl +++ b/test/rfl/system/ipc_diff.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Differential IPC oracle. ;; ;; Spawns a fresh `./rayforce -p PORT` process as an IPC server, then diff --git a/test/rfl/system/ipc_first_last.rfl b/test/rfl/system/ipc_first_last.rfl index fcd366424..b468e0bda 100644 --- a/test/rfl/system/ipc_first_last.rfl +++ b/test/rfl/system/ipc_first_last.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Regression: `first`/`last` returned over IPC used to hang the client ;; forever (issue #285). ;; diff --git a/test/rfl/system/ipc_open_errors.rfl b/test/rfl/system/ipc_open_errors.rfl index 525da04fd..7760319a0 100644 --- a/test/rfl/system/ipc_open_errors.rfl +++ b/test/rfl/system/ipc_open_errors.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; ipc_open_errors.rfl — `.ipc.open` names the failure it actually hit. ;; ;; Regression (#472): the connect and the wire handshake share one budget, diff --git a/test/rfl/system/ipc_open_timeout.rfl b/test/rfl/system/ipc_open_timeout.rfl index 6a525b1b6..896254d6b 100644 --- a/test/rfl/system/ipc_open_timeout.rfl +++ b/test/rfl/system/ipc_open_timeout.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Connect timeout argument for `.ipc.open` (issue #286). ;; ;; `.ipc.open` is now a variadic builtin accepting an optional second diff --git a/test/rfl/system/listen_fatal.rfl b/test/rfl/system/listen_fatal.rfl index 7417ae327..d4f93aa4a 100644 --- a/test/rfl/system/listen_fatal.rfl +++ b/test/rfl/system/listen_fatal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; listen_fatal.rfl — a `-p` that cannot bind is fatal (#473). ;; ;; Regression: when ray_ipc_listen_at failed, main printed diff --git a/test/rfl/system/load_errors.rfl b/test/rfl/system/load_errors.rfl index f2fdcf822..99959bf6a 100644 --- a/test/rfl/system/load_errors.rfl +++ b/test/rfl/system/load_errors.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; load_errors.rfl — a failing `load` names the file and the OS cause (#505). ;; ;; Regression: ray_load_file_fn returned a bare `io` for every failure, so diff --git a/test/rfl/system/load_home.rfl b/test/rfl/system/load_home.rfl index 4ce8575eb..febcad8e4 100644 --- a/test/rfl/system/load_home.rfl +++ b/test/rfl/system/load_home.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; load_home.rfl — relative `load` paths fall back to RAYFORCE_HOME, and ;; (.sys.args) reports the file being evaluated as `source` (#506). ;; diff --git a/test/rfl/system/log_journal.rfl b/test/rfl/system/log_journal.rfl index bca8730fd..a6d1e7534 100644 --- a/test/rfl/system/log_journal.rfl +++ b/test/rfl/system/log_journal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Journal end-to-end — transaction-log journaling (-l/-L). ;; ;; Two named processes are spawned in sequence under -l ; this diff --git a/test/rfl/system/log_journal_advanced.rfl b/test/rfl/system/log_journal_advanced.rfl index 7d1a7e228..e0916179c 100644 --- a/test/rfl/system/log_journal_advanced.rfl +++ b/test/rfl/system/log_journal_advanced.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Journal feature — invariants the basic test (log_journal.rfl) does ;; not cover. Each phase uses a distinct base + port so a phase ;; failing mid-run doesn't pollute the next one's state. diff --git a/test/rfl/system/os_fs.rfl b/test/rfl/system/os_fs.rfl index 9bccd27e9..76630f81f 100644 --- a/test/rfl/system/os_fs.rfl +++ b/test/rfl/system/os_fs.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; .fs.size and .fs.list — filesystem metadata primitives, issue #36. ;; ;; Two functions on purpose: every other predicate (exists, is-file, diff --git a/test/rfl/system/part.rfl b/test/rfl/system/part.rfl index 9084f24f3..0af053193 100644 --- a/test/rfl/system/part.rfl +++ b/test/rfl/system/part.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; src/store/part.c — exercise every reachable branch of ray_read_parted ;; (the function backing .db.parted.get) plus the ;; static helpers infer_mc_type, parse_date_dir, parse_int_dir, diff --git a/test/rfl/system/part_branch_cov.rfl b/test/rfl/system/part_branch_cov.rfl index 9553fc943..5b71277d4 100644 --- a/test/rfl/system/part_branch_cov.rfl +++ b/test/rfl/system/part_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; src/store/part.c — branch-coverage top-up for ray_read_parted and its ;; static helpers (is_date_dir, is_integer_str, infer_mc_type, ;; parse_date_dir, parse_int_dir, collect_part_dirs). diff --git a/test/rfl/system/piped_timers.rfl b/test/rfl/system/piped_timers.rfl index 83cb6a64b..689987ce2 100644 --- a/test/rfl/system/piped_timers.rfl +++ b/test/rfl/system/piped_timers.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; piped_timers.rfl — timers keep firing while a piped (non-TTY) stdin is ;; held open, and a process stays for its pending timers after input ends ;; (#493, the narrowed report). diff --git a/test/rfl/system/process_identity.rfl b/test/rfl/system/process_identity.rfl index d0baa387d..af3e5183c 100644 --- a/test/rfl/system/process_identity.rfl +++ b/test/rfl/system/process_identity.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Process and host identity, plus .sys.exec output capture — issue #573. ;; ;; An embedded process could not learn anything about itself: no PID, no diff --git a/test/rfl/system/querylog_ipc.rfl b/test/rfl/system/querylog_ipc.rfl index f65b18c20..08dd04fb2 100644 --- a/test/rfl/system/querylog_ipc.rfl +++ b/test/rfl/system/querylog_ipc.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Query-statistics ring end-to-end over IPC (the server / cloud path). ;; ;; The capture hook lives in eval_payload_core (src/core/ipc.c), so it only diff --git a/test/rfl/system/read_csv.rfl b/test/rfl/system/read_csv.rfl index 5ecd1080e..fcbe301f2 100644 --- a/test/rfl/system/read_csv.rfl +++ b/test/rfl/system/read_csv.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Ported from test_lang_rf.inc::test_rf_read_csv. ;; Rayfall's str-pool hits "error: limit" when raze/fold accumulates ;; ~1000 strings, so we shell out via .sys.exec to write the 20k-row diff --git a/test/rfl/system/startup_script_fatal.rfl b/test/rfl/system/startup_script_fatal.rfl index 343738563..d935cc273 100644 --- a/test/rfl/system/startup_script_fatal.rfl +++ b/test/rfl/system/startup_script_fatal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; startup_script_fatal.rfl — a failing startup script under -p is fatal ;; non-interactively (#507). ;; diff --git a/test/rfl/system/system_branch_cov.rfl b/test/rfl/system/system_branch_cov.rfl index 32adf1dd5..bb67d9ed6 100644 --- a/test/rfl/system/system_branch_cov.rfl +++ b/test/rfl/system/system_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; system_branch_cov.rfl — branch coverage for src/ops/system.c (part 1). ;; ;; Covers: ser/de, splayed set/get, parted get, os.size, os.list, diff --git a/test/rfl/system/timer_overrun.rfl b/test/rfl/system/timer_overrun.rfl index 9bd677297..125b2254d 100644 --- a/test/rfl/system/timer_overrun.rfl +++ b/test/rfl/system/timer_overrun.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; timer_overrun.rfl — a periodic timer does not replay missed intervals, ;; and a failing callback's line carries the message (#474). ;; diff --git a/test/stress_store.c b/test/stress_store.c index 71ac326df..c92453e4b 100644 --- a/test/stress_store.c +++ b/test/stress_store.c @@ -4,6 +4,11 @@ * Rayforce heap under test. */ +#if !defined(_WIN32) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE /* lstat under strict -std=c17 */ +#endif + +#include "test.h" /* POSIX shims on Windows (lstat) */ #include "stress_store.h" #include "store/splay.h" #include "store/part.h" @@ -14,6 +19,9 @@ #include #include #include /* getpid — per-process scratch paths */ +#include +#include +#include "store/fileio.h" /* ray_mkdir_p */ const char* stress_db_path(const char* name) { static char buf[256]; @@ -148,10 +156,28 @@ void stress_part_dir(const stress_ctx_t* c, int i, char* buf, size_t n) { snprintf(buf, n, "%s/%s/hist", c->db_root, c->part_dates[i]); } +/* Recursive delete without a shell (portable to Windows, where system() + * runs cmd.exe and has no `rm -rf`). */ static void rm_rf(const char* path) { - char cmd[600]; - snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path); - (void)!system(cmd); + struct stat st; + if (lstat(path, &st) != 0) return; /* never follow a symlink out */ + if (S_ISDIR(st.st_mode)) { + DIR* d = opendir(path); + if (d) { + struct dirent* ent; + char child[1024]; + while ((ent = readdir(d)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + continue; + snprintf(child, sizeof(child), "%s/%s", path, ent->d_name); + rm_rf(child); + } + closedir(d); + } + (void)rmdir(path); + } else { + (void)unlink(path); + } } /* ---- ray table <-> shadow rows ------------------------------------------ */ @@ -276,9 +302,7 @@ bool stress_init(stress_ctx_t* c, const char* db_root, uint64_t seed) { c->oplog = (char(*)[128])malloc((size_t)STRESS_OPLOG_CAP * 128); if (!c->oplog) return false; rm_rf(c->db_root); - char cmd[600]; - snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", c->db_root); - if (system(cmd) != 0) { + if (ray_mkdir_p(c->db_root) != RAY_OK) { free(c->oplog); c->oplog = NULL; return false; diff --git a/test/test.h b/test/test.h index 8a54c42ee..8f67db58d 100644 --- a/test/test.h +++ b/test/test.h @@ -41,6 +41,60 @@ #include #include +/* Shell-free filesystem helpers (test/main.c). Tests must not depend on + * /bin/sh: on Windows system() runs cmd.exe. Both return 0 on success. */ +int ray_test_rm_rf(const char* path); /* rm -rf path */ +int ray_test_mkdir_p(const char* path); /* mkdir -p path */ + +#if defined(_WIN32) +/* POSIX helpers the tests rely on, mapped onto their MSVCRT equivalents. + * Test paths use "/tmp/...", which Windows resolves to :\tmp; the + * runner creates that directory at startup (see test/main.c). */ +#include +#include +#include +#include +static inline int setenv(const char* k, const char* v, int overwrite) { + if (!overwrite && getenv(k)) return 0; + return _putenv_s(k, v) == 0 ? 0 : -1; +} +static inline int unsetenv(const char* k) { + return _putenv_s(k, "") == 0 ? 0 : -1; /* "" removes the variable */ +} +static inline char* mkdtemp(char* tmpl) { + if (!_mktemp(tmpl)) return NULL; + return _mkdir(tmpl) == 0 ? tmpl : NULL; +} +static inline unsigned geteuid(void) { return 1; } /* never "root" */ +#define lstat stat /* no symlinks to skip */ +#include +#include +static inline int symlink(const char* target, const char* path) { + (void)target; (void)path; + errno = ENOSYS; /* callers skip when symlink fails */ + return -1; +} +#define mkdir(p, mode) _mkdir(p) +#define pipe(fds) _pipe((fds), 65536, _O_BINARY) +/* sysconf subset (page size, physical pages, CPUs); see test/main.c. */ +#define _SC_PAGESIZE 1 +#define _SC_PAGE_SIZE _SC_PAGESIZE +#define _SC_PHYS_PAGES 2 +#define _SC_NPROCESSORS_ONLN 3 +long ray_test_sysconf(int name); +#define sysconf ray_test_sysconf +/* MSVCRT's tmpfile() creates its file in the drive root, which needs admin + * rights. Use the temp directory instead; "D" deletes the file on close. */ +static inline FILE* ray_test_tmpfile(void) { + char* name = _tempnam("/tmp", "rayt"); + if (!name) return NULL; + FILE* f = fopen(name, "w+bD"); + free(name); + return f; +} +#define tmpfile ray_test_tmpfile +#endif + typedef enum { TEST_PASS = 0, TEST_FAIL, TEST_SKIP } test_status_t; typedef struct { diff --git a/test/test_aof.c b/test/test_aof.c index 4705436fc..469f5491e 100644 --- a/test/test_aof.c +++ b/test/test_aof.c @@ -58,7 +58,12 @@ static void aof_rm_rf(const char* dir) { } static void aof_setup(void) { - snprintf(g_aof_dir, sizeof g_aof_dir, "/tmp/ray_test_aof_%d", (int)getpid()); + /* A fresh dir per test: the crash test deliberately leaks an open + * writer, and on Windows an open file cannot be deleted, so a shared + * dir would hand its stale segment to every later test. */ + static int seq = 0; + snprintf(g_aof_dir, sizeof g_aof_dir, "/tmp/ray_test_aof_%d_%d", + (int)getpid(), seq++); aof_rm_rf(g_aof_dir); } diff --git a/test/test_csv.c b/test/test_csv.c index 9a964fc23..3f0156246 100644 --- a/test/test_csv.c +++ b/test/test_csv.c @@ -213,7 +213,7 @@ static test_result_t test_csv_null_i64(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n\n30\n"); fclose(f); @@ -245,7 +245,7 @@ static test_result_t test_csv_null_i64_unparseable(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\nN/A\n30\n"); fclose(f); @@ -274,7 +274,7 @@ static test_result_t test_csv_null_f64(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n1.5\n\n3.5\n"); fclose(f); @@ -305,7 +305,7 @@ static test_result_t test_csv_null_i16(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n\n30\n"); fclose(f); @@ -336,7 +336,7 @@ static test_result_t test_csv_null_i32(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n\n30\n"); fclose(f); @@ -367,7 +367,7 @@ static test_result_t test_csv_null_date(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "d\n2025-01-02\n\n2026-12-31\n"); fclose(f); @@ -396,7 +396,7 @@ static test_result_t test_csv_null_time(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "t\n12:34:56\n\n23:59:59\n"); fclose(f); @@ -425,7 +425,7 @@ static test_result_t test_csv_null_timestamp(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "ts\n2025-01-02T03:04:05\n\n2026-12-31T23:59:59\n"); fclose(f); @@ -455,7 +455,7 @@ static test_result_t test_csv_null_bool(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "flag\ntrue\n\nfalse\n"); fclose(f); @@ -484,7 +484,7 @@ static test_result_t test_csv_null_sym(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "name\nalice\n\nbob\n"); fclose(f); @@ -515,7 +515,7 @@ static test_result_t test_csv_no_nulls_no_null_bitmap(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n20\n30\n"); fclose(f); @@ -537,7 +537,7 @@ static test_result_t test_csv_null_mixed_columns(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "id,val,name\n1,1.5,alice\n,2.5,\n3,,bob\n"); fclose(f); @@ -579,7 +579,7 @@ static test_result_t test_csv_explicit_str_schema(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); /* Mix inline (<=12B), pooled (>12B), empty/null, and a short */ fprintf(f, "id,note\n" "1,hi\n" @@ -628,7 +628,7 @@ static test_result_t test_csv_escaped_str_roundtrip(void) { (void)ray_sym_init(); /* Write a CSV with fields that require quoting/escaping */ - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "s\n" "\"he,llo\"\n" "\"wo\"\"rld\"\n" @@ -748,7 +748,7 @@ static test_result_t test_csv_infer_date(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "d\n2025-01-02\n2026-12-31\n2000-03-15\n"); fclose(f); @@ -769,7 +769,7 @@ static test_result_t test_csv_infer_time(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "t\n12:34:56\n00:00:00\n23:59:59.123\n"); fclose(f); @@ -792,7 +792,7 @@ static test_result_t test_csv_infer_timestamp_promotion(void) { /* Mix of full timestamps with both 'T' and ' ' separators, plus a * date-only sentinel that should be promoted to TIMESTAMP. */ - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "ts\n2025-01-02T03:04:05\n2025-06-07 08:09:10.123456\n2024-12-31\n"); fclose(f); @@ -813,7 +813,7 @@ static test_result_t test_csv_infer_bool(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "b\ntrue\nfalse\nTRUE\nFALSE\n"); fclose(f); @@ -834,7 +834,7 @@ static test_result_t test_csv_infer_f64_specials(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n1.0\n2e10\n-3.5E-2\nnan\nInf\n+inf\n-INF\n"); fclose(f); @@ -856,7 +856,7 @@ static test_result_t test_csv_infer_null_sentinels(void) { (void)ray_sym_init(); /* Sentinel rows alternating with i64 values; column should infer I64. */ - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\nN/A\nNA\nnull\nNULL\nNone\nnone\nn/a\nna\n.\n42\n"); fclose(f); @@ -882,7 +882,7 @@ static test_result_t test_csv_infer_promotions(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "n,b\n1,true\n2,0\n3.5,1\n"); fclose(f); @@ -905,7 +905,7 @@ static test_result_t test_csv_tab_delimiter(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "a\tb\tc\n1\t2\t3\n4\t5\t6\n"); fclose(f); @@ -926,7 +926,7 @@ static test_result_t test_csv_no_header(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "10,20\n30,40\n50,60\n"); fclose(f); @@ -988,7 +988,7 @@ static test_result_t test_csv_invalid_schema_type(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n1\n2\n"); fclose(f); @@ -1002,7 +1002,7 @@ static test_result_t test_csv_invalid_schema_type(void) { /* Schema too short for ncols also errors out. */ int8_t one_only[1] = { RAY_I64 }; - FILE* g = fopen(TMP_CSV, "w"); + FILE* g = fopen(TMP_CSV, "wb"); fprintf(g, "a,b\n1,2\n"); fclose(g); ray_t* loaded3 = ray_read_csv_opts(TMP_CSV, ',', true, one_only, 1); @@ -1044,7 +1044,7 @@ static test_result_t test_csv_truncated_row(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "a,b,c\n1,2,3\n4\n7,8,9\n"); fclose(f); @@ -1277,7 +1277,7 @@ static test_result_t test_csv_parallel_parse(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "i,s\n"); /* 9000 rows so n_rows > 8192. */ for (int i = 0; i < 9000; i++) @@ -1306,7 +1306,7 @@ static test_result_t test_csv_sym_narrowing(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "k\n"); /* Only three distinct values across many rows. */ for (int i = 0; i < 200; i++) @@ -1342,7 +1342,7 @@ static test_result_t test_csv_explicit_u8_schema(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n"); /* 10 000 rows ⇒ parallel parse path; values 0..255 cycling so the * truncated bytes fully exercise the U8 range. */ @@ -1384,7 +1384,7 @@ static test_result_t test_csv_explicit_i16_schema_with_nulls(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n"); const int N = 1500; for (int i = 0; i < N; i++) { @@ -1427,7 +1427,7 @@ static test_result_t test_csv_explicit_i32_schema(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n"); const int N = 500; for (int i = 0; i < N; i++) fprintf(f, "%d\n", -100000 + i * 137); @@ -1460,7 +1460,7 @@ static test_result_t test_csv_explicit_u8_schema_serial(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "a,b\n"); /* 200 rows; second column missing on every 50th row → triggers * past-row-boundary fill in the parser. */ @@ -1501,7 +1501,7 @@ static test_result_t test_csv_infer_high_cardinality_str(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "payload\n"); for (int i = 0; i < 100; i++) fprintf(f, "unique_payload_%03d\n", i); @@ -1570,7 +1570,7 @@ static test_result_t test_csv_interrupt_mid_parse(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); TEST_ASSERT_NOT_NULL(f); fputs("id,payload\n", f); for (int i = 0; i < 200000; i++) { @@ -1610,7 +1610,7 @@ static test_result_t test_csv_progress_never_goes_backwards(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); TEST_ASSERT_NOT_NULL(f); fputs("payload,symbol\n", f); for (int i = 0; i < 100000; i++) diff --git a/test/test_heap.c b/test/test_heap.c index 2054f7b15..0e14473f2 100644 --- a/test/test_heap.c +++ b/test/test_heap.c @@ -50,7 +50,33 @@ #include #include #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +/* Anonymous mmap for the fake file-mapped (mmod==1) blocks below: a + * pagefile-backed view, so the library's ray_vm_unmap_file + * (UnmapViewOfFile) releases it just as it would a real file mapping. */ +#define PROT_READ 1 +#define PROT_WRITE 2 +#define MAP_PRIVATE 2 +#define MAP_ANONYMOUS 0x20 +#define MAP_FAILED ((void*)-1) +static void* mmap(void* addr, size_t len, int prot, int flags, int fd, long off) { + (void)addr; (void)prot; (void)flags; (void)fd; (void)off; + HANDLE m = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, + 0, (DWORD)len, NULL); + if (!m) return MAP_FAILED; + void* p = MapViewOfFile(m, FILE_MAP_WRITE, 0, 0, len); + CloseHandle(m); + return p ? p : MAP_FAILED; +} +static int munmap(void* p, size_t len) { + (void)len; + return UnmapViewOfFile(p) ? 0 : -1; +} +#else #include +#endif #include #include @@ -2565,6 +2591,11 @@ static test_result_t test_direct_cache_concurrent_replacement(void) { /* Drive the anon-to-file crossing with a low watermark, independent of RAM. */ static test_result_t test_anon_watermark_spill(void) { +#if defined(_WIN32) + /* Windows takes only the anonymous path; the file-backed spill is + * POSIX-only (docs/architecture/memory.md, RAY_HEAP_FILE_SPILL). */ + SKIP("file-backed spill is POSIX-only"); +#endif size_t sz = 40 * 1024 * 1024 - 128; /* order 26 → direct path */ /* Start from an empty reuse cache: leftover cached blocks from earlier * tests would (a) inflate the baseline and (b) be drained by the diff --git a/test/test_ipc.c b/test/test_ipc.c index 21b3dfa1c..aa1752f06 100644 --- a/test/test_ipc.c +++ b/test/test_ipc.c @@ -54,11 +54,16 @@ #include "test.h" #include "ipc_harness.h" +#ifdef RAY_OS_WINDOWS +#include +#include +#else #include #include #include #include #include +#endif #include #include "core/ipc.h" #include "core/sock.h" @@ -1047,12 +1052,17 @@ static test_result_t test_ipc_send_large_compressible(void) { * Open a journal, then connect an IPC server on top; each SYNC message * should flow through ray_journal_write_bytes. */ +/* Drop a journal's .log / .qdb pair (no shell). */ +static void ipc_rm_journal(const char* jbase) { + char path[256]; + snprintf(path, sizeof(path), "%s.log", jbase); (void)remove(path); + snprintf(path, sizeof(path), "%s.qdb", jbase); (void)remove(path); +} + static test_result_t test_ipc_journal_path(void) { const char* jbase = "/tmp/rayforce_test_ipc_journal"; /* Remove stale files */ - char cmd[256]; - snprintf(cmd, sizeof(cmd), "rm -f %s.log %s.qdb", jbase, jbase); - system(cmd); + ipc_rm_journal(jbase); /* Open journal */ ray_err_t jerr = ray_journal_open(jbase, RAY_JOURNAL_ASYNC); @@ -1081,7 +1091,7 @@ static test_result_t test_ipc_journal_path(void) { ray_test_server_stop(&srv); ray_journal_close(); - system(cmd); /* cleanup */ + ipc_rm_journal(jbase); /* cleanup */ PASS(); } @@ -1453,9 +1463,7 @@ static test_result_t test_ipc_send_verbose_large_result(void) { */ static test_result_t test_ipc_journal_restricted(void) { const char* jbase = "/tmp/rayforce_test_ipc_jrestr"; - char cmd[256]; - snprintf(cmd, sizeof(cmd), "rm -f %s.log %s.qdb", jbase, jbase); - system(cmd); + ipc_rm_journal(jbase); ray_err_t jerr = ray_journal_open(jbase, RAY_JOURNAL_ASYNC); if (jerr != RAY_OK) { @@ -1486,7 +1494,7 @@ static test_result_t test_ipc_journal_restricted(void) { ray_test_server_stop(&srv); ray_journal_close(); - system(cmd); + ipc_rm_journal(jbase); PASS(); } @@ -1979,6 +1987,7 @@ static test_result_t test_ipc_addr_local_ipv6_remote(void) { PASS(); } +#ifndef RAY_OS_WINDOWS /* AF_UNIX locality is POSIX-only (see sock.c) */ static test_result_t test_ipc_addr_local_af_unix(void) { struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); @@ -1986,6 +1995,7 @@ static test_result_t test_ipc_addr_local_af_unix(void) { TEST_ASSERT_TRUE(ray_sock_addr_is_local(&sa, sizeof(sa))); PASS(); } +#endif static test_result_t test_ipc_addr_local_rejects_garbage(void) { struct sockaddr_in sa; @@ -1998,24 +2008,53 @@ static test_result_t test_ipc_addr_local_rejects_garbage(void) { PASS(); } -/* A real AF_UNIX pair resolves as local through getpeername. */ +/* A connected pair of sockets on this machine: an AF_UNIX socketpair on + * POSIX; Windows has no socketpair(2), so a loopback TCP pair there. */ +static int test_local_sock_pair(ray_sock_t sv[2]) { +#ifdef RAY_OS_WINDOWS + ray_sock_t srv = ray_sock_listen_at("127.0.0.1", 0); + if (srv == RAY_INVALID_SOCK) return -1; + struct sockaddr_in addr; + int len = sizeof(addr); + if (getsockname((SOCKET)srv, (struct sockaddr*)&addr, &len) != 0) { + ray_sock_close(srv); + return -1; + } + sv[0] = ray_sock_connect("127.0.0.1", ntohs(addr.sin_port), 0); + sv[1] = sv[0] == RAY_INVALID_SOCK ? RAY_INVALID_SOCK : ray_sock_accept(srv); + ray_sock_close(srv); + if (sv[1] == RAY_INVALID_SOCK) { + if (sv[0] != RAY_INVALID_SOCK) ray_sock_close(sv[0]); + return -1; + } + return 0; +#else + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) return -1; + sv[0] = fds[0]; + sv[1] = fds[1]; + return 0; +#endif +} + +/* A real local pair resolves as local through getpeername. */ static test_result_t test_ipc_peer_is_local_socketpair(void) { - int sv[2]; - TEST_ASSERT_EQ_I(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); - TEST_ASSERT_TRUE(ray_sock_peer_is_local((ray_sock_t)sv[0])); - TEST_ASSERT_TRUE(ray_sock_peer_is_local((ray_sock_t)sv[1])); - close(sv[0]); - close(sv[1]); + ray_sock_t sv[2]; + TEST_ASSERT_EQ_I(test_local_sock_pair(sv), 0); + TEST_ASSERT_TRUE(ray_sock_peer_is_local(sv[0])); + TEST_ASSERT_TRUE(ray_sock_peer_is_local(sv[1])); + ray_sock_close(sv[0]); + ray_sock_close(sv[1]); PASS(); } /* An unconnected socket has no peer: getpeername fails, and an unknown * peer must fall back to the compressing default, not to "local". */ static test_result_t test_ipc_peer_is_local_unconnected(void) { - int fd = socket(AF_INET, SOCK_STREAM, 0); - TEST_ASSERT_TRUE(fd >= 0); - TEST_ASSERT_FALSE(ray_sock_peer_is_local((ray_sock_t)fd)); - close(fd); + ray_sock_t fd = (ray_sock_t)socket(AF_INET, SOCK_STREAM, 0); + TEST_ASSERT_TRUE(fd != RAY_INVALID_SOCK); + TEST_ASSERT_FALSE(ray_sock_peer_is_local(fd)); + ray_sock_close(fd); PASS(); } @@ -2027,18 +2066,18 @@ static test_result_t test_ipc_peer_is_local_invalid_fd(void) { /* The policy the send paths consult: local links never compress, others * keep the compiled-in default. */ static test_result_t test_ipc_link_threshold_local_vs_remote(void) { - int sv[2]; - TEST_ASSERT_EQ_I(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); - TEST_ASSERT_EQ_U(ray_ipc_link_threshold((ray_sock_t)sv[0]), + ray_sock_t sv[2]; + TEST_ASSERT_EQ_I(test_local_sock_pair(sv), 0); + TEST_ASSERT_EQ_U(ray_ipc_link_threshold(sv[0]), RAY_IPC_COMPRESS_NEVER); - close(sv[0]); - close(sv[1]); + ray_sock_close(sv[0]); + ray_sock_close(sv[1]); - int fd = socket(AF_INET, SOCK_STREAM, 0); - TEST_ASSERT_TRUE(fd >= 0); - TEST_ASSERT_EQ_U(ray_ipc_link_threshold((ray_sock_t)fd), + ray_sock_t fd = (ray_sock_t)socket(AF_INET, SOCK_STREAM, 0); + TEST_ASSERT_TRUE(fd != RAY_INVALID_SOCK); + TEST_ASSERT_EQ_U(ray_ipc_link_threshold(fd), (size_t)RAY_IPC_COMPRESS_THRESHOLD); - close(fd); + ray_sock_close(fd); PASS(); } @@ -2747,7 +2786,9 @@ const test_entry_t ipc_entries[] = { { "ipc/addr_local/ipv6_loopback", test_ipc_addr_local_ipv6_loopback, ipc_setup, ipc_teardown }, { "ipc/addr_local/ipv6_mapped_loopback",test_ipc_addr_local_ipv6_mapped_loopback, ipc_setup, ipc_teardown }, { "ipc/addr_local/ipv6_remote", test_ipc_addr_local_ipv6_remote, ipc_setup, ipc_teardown }, +#ifndef RAY_OS_WINDOWS { "ipc/addr_local/af_unix", test_ipc_addr_local_af_unix, ipc_setup, ipc_teardown }, +#endif { "ipc/addr_local/rejects_garbage", test_ipc_addr_local_rejects_garbage, ipc_setup, ipc_teardown }, { "ipc/peer_is_local/socketpair", test_ipc_peer_is_local_socketpair, ipc_setup, ipc_teardown }, { "ipc/peer_is_local/unconnected", test_ipc_peer_is_local_unconnected, ipc_setup, ipc_teardown }, diff --git a/test/test_journal.c b/test/test_journal.c index 8277a7290..9904893f6 100644 --- a/test/test_journal.c +++ b/test/test_journal.c @@ -40,7 +40,44 @@ #include #include #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +/* Minimal glob(3) for the archive checks below: wildcards only in the + * last path component, which is all ".*.log" needs. */ +typedef struct { size_t gl_pathc; char** gl_pathv; } glob_t; +static void globfree(glob_t* g) { + for (size_t i = 0; i < g->gl_pathc; i++) free(g->gl_pathv[i]); + free(g->gl_pathv); + g->gl_pathc = 0; g->gl_pathv = NULL; +} +static int glob(const char* pat, int flags, void* errfunc, glob_t* g) { + (void)flags; (void)errfunc; + g->gl_pathc = 0; g->gl_pathv = NULL; + const char* slash = strrchr(pat, '/'); + const char* bs = strrchr(pat, '\\'); + if (bs && (!slash || bs > slash)) slash = bs; + size_t dlen = slash ? (size_t)(slash - pat) + 1 : 0; + WIN32_FIND_DATAA fd; + HANDLE h = FindFirstFileA(pat, &fd); + if (h == INVALID_HANDLE_VALUE) return 3; /* GLOB_NOMATCH */ + do { + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; + size_t nlen = strlen(fd.cFileName); + char* path = (char*)malloc(dlen + nlen + 1); + char** v = (char**)realloc(g->gl_pathv, (g->gl_pathc + 1) * sizeof(char*)); + if (!path || !v) { free(path); if (v) g->gl_pathv = v; FindClose(h); globfree(g); return 1; } + memcpy(path, pat, dlen); + memcpy(path + dlen, fd.cFileName, nlen + 1); + g->gl_pathv = v; + g->gl_pathv[g->gl_pathc++] = path; + } while (FindNextFileA(h, &fd)); + FindClose(h); + return g->gl_pathc ? 0 : 3; +} +#else #include +#endif /* ── Runtime fixture (same pattern as test_link.c) ─────────────────── */ @@ -98,8 +135,17 @@ static void cleanup_base(const char* base) { snprintf(path, sizeof(path), "%s.qdb", base); unlink(path); snprintf(path, sizeof(path), "%s.qdb.tmp", base); unlink(path); /* Archived rolls have the form base..log — remove with glob via shell. */ +#if defined(_WIN32) + snprintf(path, sizeof(path), "%s.*.log", base); /* no POSIX shell here */ + glob_t g; + if (glob(path, 0, NULL, &g) == 0) { + for (size_t i = 0; i < g.gl_pathc; i++) unlink(g.gl_pathv[i]); + globfree(&g); + } +#else snprintf(path, sizeof(path), "rm -f '%s'.*.log 2>/dev/null", base); (void)system(path); +#endif } /* ═══════════════════════════════════════════════════════════════════════ @@ -1041,15 +1087,19 @@ static test_result_t test_journal_crash_window_no_double_apply(void) { /* Simulate the crash IN the window: put the just-archived log back * under its live name, exactly the on-disk state a crash between - * the two renames leaves behind. */ + * the two renames leaves behind. The journal is closed first (the + * "process" is gone), and the live file removed before the rename: + * Windows can neither replace a file that is still open nor rename + * onto an existing one. */ + TEST_ASSERT_EQ_I(ray_journal_close(), RAY_OK); char pattern[300]; snprintf(pattern, sizeof(pattern), "%s.*.log", base); glob_t g; TEST_ASSERT_EQ_I(glob(pattern, 0, NULL, &g), 0); TEST_ASSERT_EQ_I((int64_t)g.gl_pathc, 1); + (void)remove(lpath); TEST_ASSERT_EQ_I(rename(g.gl_pathv[0], lpath), 0); globfree(&g); - TEST_ASSERT_EQ_I(ray_journal_close(), RAY_OK); /* Restart: clobber the binding, recover. The covered log must be * skipped — jw_x comes back as the snapshot value, not value+1. */ @@ -2294,9 +2344,13 @@ static bool purge_write_one(int64_t x) { /* True iff at least one rolled archive (base..log) exists. */ static bool archive_exists(const char* base) { - char cmd[1200]; - snprintf(cmd, sizeof(cmd), "test -n \"$(ls '%s'.*.log 2>/dev/null)\"", base); - return system(cmd) == 0; + char pattern[1200]; + snprintf(pattern, sizeof(pattern), "%s.*.log", base); + glob_t g; + if (glob(pattern, 0, NULL, &g) != 0) return false; /* no match */ + bool found = g.gl_pathc > 0; + globfree(&g); + return found; } /* P1. Full purge while the journal is OPEN: closes it, unlinks the active diff --git a/test/test_link.c b/test/test_link.c index cd430538c..7f1c8d560 100644 --- a/test/test_link.c +++ b/test/test_link.c @@ -918,9 +918,7 @@ static ray_err_t write_link_partition(const char* part_dir, int64_t custs_sym) { char dir[1024]; snprintf(dir, sizeof(dir), TMP_LINK_PART_DB "/%s/" TMP_LINK_PART_TBL, part_dir); - char cmd[1100]; - snprintf(cmd, sizeof(cmd), "mkdir -p %s", dir); - if (system(cmd) != 0) return RAY_ERR_IO; + if (ray_test_mkdir_p(dir) != 0) return RAY_ERR_IO; ray_t* ridcol = ray_vec_from_raw(RAY_I64, (void*)rids, n_rid); if (!ridcol || RAY_IS_ERR(ridcol)) return RAY_ERR_OOM; @@ -954,7 +952,7 @@ static ray_err_t write_link_partition(const char* part_dir, static test_result_t test_link_parted_load_propagates(void) { int64_t custs_sym = setup_custs_dim(); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1, 2 }; int64_t q1[] = { 10, 20, 30 }; @@ -1020,7 +1018,7 @@ static test_result_t test_link_parted_load_propagates(void) { ray_release(ages1); ray_release(parted); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1032,7 +1030,7 @@ static test_result_t test_link_attach_rejects_parted_target(void) { /* Build a parted table on disk and load via ray_read_parted so we have a * real RAY_TABLE-with-RAY_PARTED-cols handle to point at. */ int64_t custs_sym = setup_custs_dim(); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1, 2 }; int64_t q1[] = { 10, 20, 30 }; @@ -1065,7 +1063,7 @@ static test_result_t test_link_attach_rejects_parted_target(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1095,7 +1093,7 @@ static test_result_t test_link_deref_rejects_parted_after_rebind(void) { ray_release(good); /* Build a parted table on disk and rebind `custs` to it. */ - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1 }; int64_t q1[] = { 10, 20 }; TEST_ASSERT_EQ_I(write_link_partition("2024.01.01", r1, 2, q1, 2, custs_sym), RAY_OK); @@ -1118,7 +1116,7 @@ static test_result_t test_link_deref_rejects_parted_after_rebind(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1155,7 +1153,7 @@ static test_result_t test_link_dotted_resolve_propagates_parted_error(void) { ray_release(good); /* Rebind custs to a parted table on disk. */ - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1 }; int64_t q1[] = { 10, 20 }; TEST_ASSERT_EQ_I(write_link_partition("2024.01.01", r1, 2, q1, 2, custs_sym), RAY_OK); @@ -1180,7 +1178,7 @@ static test_result_t test_link_dotted_resolve_propagates_parted_error(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1212,7 +1210,7 @@ static test_result_t test_link_vm_eval_propagates_parted_error(void) { ray_release(good); /* Rebind custs to a parted table. */ - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1 }; int64_t q1[] = { 10, 20 }; TEST_ASSERT_EQ_I(write_link_partition("2024.01.01", r1, 2, q1, 2, custs_sym), RAY_OK); @@ -1233,7 +1231,7 @@ static test_result_t test_link_vm_eval_propagates_parted_error(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } diff --git a/test/test_mcast.c b/test/test_mcast.c index 8fa810535..e71e174bf 100644 --- a/test/test_mcast.c +++ b/test/test_mcast.c @@ -26,6 +26,9 @@ #include #include #include +#else + #include + #include #endif extern ray_runtime_t* __RUNTIME; @@ -631,7 +634,6 @@ static test_result_t test_mcast_large_payload_queues_until_writable(void) { TEST_ASSERT_FALSE(RAY_IS_ERR(sub)); ray_release(sub); -#ifndef RAY_OS_WINDOWS ray_t* server_h = ray_env_get(ray_sym_intern("_mc_sub_handle", 14)); TEST_ASSERT_NOT_NULL(server_h); TEST_ASSERT_EQ_I(server_h->type, -RAY_I64); @@ -639,8 +641,7 @@ static test_result_t test_mcast_large_payload_queues_until_writable(void) { TEST_ASSERT_NOT_NULL(server_sel); int sndbuf = 4096; setsockopt((ray_sock_t)server_sel->fd, SOL_SOCKET, SO_SNDBUF, - &sndbuf, sizeof(sndbuf)); -#endif + (const char*)&sndbuf, sizeof(sndbuf)); const char* pub_src = "(.mc.pub \"big\" (+ (* (til 100000) 1103515245) 12345))"; @@ -728,7 +729,6 @@ static test_result_t test_mcast_shared_frame_across_subscribers(void) { int64_t hp = ray_ipc_connect("127.0.0.1", port, NULL, NULL, 0); TEST_ASSERT((hp) >= (0), "publisher connected"); -#ifndef RAY_OS_WINDOWS ray_t* server_hs = ray_env_get(ray_sym_intern("_mc_handles", 11)); TEST_ASSERT_NOT_NULL(server_hs); TEST_ASSERT((ray_len(server_hs)) >= (3), "three server-side subscriber handles"); @@ -737,9 +737,8 @@ static test_result_t test_mcast_shared_frame_across_subscribers(void) { ray_selector_t* ssel = ray_poll_get(poll, sh); TEST_ASSERT_NOT_NULL(ssel); int sndbuf = 4096; - setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, (const char*)&sndbuf, sizeof(sndbuf)); } -#endif const char* pub_src = "(.mc.pub \"big\" (+ (* (til 100000) 1103515245) 12345))"; ray_t* msg = ray_str(pub_src, strlen(pub_src)); @@ -802,6 +801,13 @@ static test_result_t test_mcast_shared_frame_across_subscribers(void) { * frame keeps receiving. The active limits and the high-water mark are * readable from .mc.stats and per handle from (.ipc.handle h). */ static test_result_t test_mcast_txlimit_overflow_disconnects(void) { +#if defined(_WIN32) + /* The backlog this test needs never forms on Windows: Winsock accepts a + * single non-blocking send() larger than SO_SNDBUF whole (it pins the + * caller's buffer), so the 800 KiB frame leaves at once and no + * subscriber crosses its tx limit. */ + SKIP("Winsock accepts oversized sends whole; no tx backlog forms"); +#endif ray_t* r = ray_eval_str( "(set _mc_count 0)" "(set _mc_close_count 0)" @@ -840,14 +846,12 @@ static test_result_t test_mcast_txlimit_overflow_disconnects(void) { TEST_ASSERT((ray_len(server_hs)) >= (2), "two server-side subscriber handles"); int64_t s1 = ((int64_t*)ray_data(server_hs))[0]; int64_t s2 = ((int64_t*)ray_data(server_hs))[1]; -#ifndef RAY_OS_WINDOWS for (int i = 0; i < 2; i++) { ray_selector_t* ssel = ray_poll_get(poll, i == 0 ? s1 : s2); TEST_ASSERT_NOT_NULL(ssel); int sndbuf = 4096; - setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, (const char*)&sndbuf, sizeof(sndbuf)); } -#endif /* Process default: 64 KiB. h2 alone may hold 4 MiB. */ ray_t* msg = ray_str("(.ipc.txlimit 65536 0)", strlen("(.ipc.txlimit 65536 0)")); @@ -1040,14 +1044,12 @@ static test_result_t test_ipc_outbound_close_hook(void) { TEST_ASSERT_TRUE(pump_until_env_i64_at_least("_oc_out", 7, 1, 1000)); /* B: the peer resets h2 (linger zero, then close → RST) */ -#ifndef RAY_OS_WINDOWS { ray_selector_t* ssel = ray_poll_get(poll, s2); TEST_ASSERT_NOT_NULL(ssel); struct linger lg = { 1, 0 }; - setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_LINGER, (const char*)&lg, sizeof(lg)); } -#endif n = snprintf(src, sizeof(src), "(.ipc.close %lld)", (long long)s2); msg = ray_str(src, (size_t)n); cr = ray_ipc_send(hc, msg); @@ -1224,7 +1226,6 @@ static test_result_t test_mcast_sync_reply_after_queued_frame(void) { TEST_ASSERT_FALSE(RAY_IS_ERR(sub)); ray_release(sub); -#ifndef RAY_OS_WINDOWS /* Shrink the server->h1 send buffer so a big frame parks on sel->tx.buf * instead of leaving in a single write. */ ray_t* server_h = ray_env_get(ray_sym_intern("_mc_sub_handle", 14)); @@ -1234,8 +1235,7 @@ static test_result_t test_mcast_sync_reply_after_queued_frame(void) { TEST_ASSERT_NOT_NULL(server_sel); int sndbuf = 4096; setsockopt((ray_sock_t)server_sel->fd, SOL_SOCKET, SO_SNDBUF, - &sndbuf, sizeof(sndbuf)); -#endif + (const char*)&sndbuf, sizeof(sndbuf)); const char* pub_src = "(.mc.pub \"big\" (+ (* (til 100000) 1103515245) 12345))"; diff --git a/test/test_repl.c b/test/test_repl.c index a341cc4fa..5397c2070 100644 --- a/test/test_repl.c +++ b/test/test_repl.c @@ -88,10 +88,8 @@ extern void* ray_runtime_get_poll(void); * poll first (closes any leftover conns), runtime second. */ static void repl_setup(void) { ray_runtime_create(0, NULL); -#ifndef RAY_OS_WINDOWS ray_poll_t* p = ray_poll_create(); if (p) ray_runtime_set_poll(p); -#endif } static void repl_teardown(void) { @@ -103,7 +101,6 @@ static void repl_teardown(void) { ray_t* args = NULL; ray_release(ray_repl_disconnect_fn(&args, 0)); } -#ifndef RAY_OS_WINDOWS { ray_poll_t* p = (ray_poll_t*)ray_runtime_get_poll(); if (p) { @@ -111,7 +108,6 @@ static void repl_teardown(void) { ray_poll_destroy(p); } } -#endif ray_runtime_destroy(__RUNTIME); } @@ -670,7 +666,9 @@ static test_result_t test_repl_pty_ctrl_d(void) { * code proves it ran while the prompt was idle, and the helper's 5 s * timeout (-2) is what a starved timer would produce. Nothing after * the timer line is ever written to the pty. */ +#ifndef RAY_OS_WINDOWS static int run_pty_listen_with_poll(const char* input); +#endif static test_result_t test_repl_pty_timer_fires_while_idle(void) { #ifndef RAY_OS_WINDOWS int rc = run_pty_listen_with_poll("(.time.timer.set 150 1 (fn [t] (exit 7)))\n"); diff --git a/test/test_runtime.c b/test/test_runtime.c index 35160723b..e6bb4f068 100644 --- a/test/test_runtime.c +++ b/test/test_runtime.c @@ -37,8 +37,13 @@ #include #include #include +#ifdef RAY_OS_WINDOWS +#include +#include +#else #include #include +#endif static char* make_tmpdir(void) { char tmpl[] = "/tmp/rayforce-rt-test-XXXXXX"; @@ -70,6 +75,11 @@ static test_result_t test_create_with_sym_absent_is_ok(void) { * passing a path whose parent exists but isn't a directory (ENOTDIR) — * portable across Linux/macOS without needing root or chmod games. */ static test_result_t test_create_with_sym_io_error_surfaces(void) { +#if defined(_WIN32) + /* Win32 reports a path through a regular file as "path not found" + * (ENOENT, the missing-file case), never ENOTDIR. */ + SKIP("no ENOTDIR on Windows"); +#endif char* dir = make_tmpdir(); TEST_ASSERT_NOT_NULL(dir); @@ -1265,9 +1275,7 @@ static test_result_t test_syscov_splayed_set_with_sym_path(void) { } /* cleanup — no free(dir), it's a stack pointer */ - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); - system(cmd); + (void)ray_test_rm_rf(dir); PASS(); } diff --git a/test/test_splay.c b/test/test_splay.c index 1137cbbd0..34188b468 100644 --- a/test/test_splay.c +++ b/test/test_splay.c @@ -65,9 +65,7 @@ static void splay_teardown(void) { /* Remove temp dir tree */ static void rm_rf(const char* path) { - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf %s", path); - (void)!system(cmd); + (void)ray_test_rm_rf(path); } /* ========================================================================= @@ -181,9 +179,7 @@ static test_result_t test_load_missing_schema(void) { /* Directory exists but contains no .d file */ const char* dir = TMP_SPLAY_BASE "/no_schema"; rm_rf(dir); - char cmd[512]; - snprintf(cmd, sizeof(cmd), "mkdir -p %s", dir); - (void)!system(cmd); + (void)ray_test_mkdir_p(dir); ray_t* r = ray_splay_load(dir, NULL); /* ray_col_load of missing file returns an error object */ @@ -499,9 +495,7 @@ static test_result_t test_save_sym_error(void) { char sym_as_dir[512]; snprintf(sym_as_dir, sizeof(sym_as_dir), "%s/sym_dir", dir); /* Ensure parent dir exists first */ - char mk[600]; - snprintf(mk, sizeof(mk), "mkdir -p %s", sym_as_dir); - (void)!system(mk); + (void)ray_test_mkdir_p(sym_as_dir); ray_err_t err = ray_splay_save(tbl, dir, sym_as_dir); /* Either succeeds (some impls tolerate it) or returns an error — either @@ -732,8 +726,8 @@ static test_result_t test_save_bulk_with_sym_path(void) { * 19. splay_save_impl: snprintf overflow for the column / ".d" paths. * Requires strlen(dir) >= 1021 so that strlen(dir)+3 >= 1024. * Build a deeply nested path using short components (≤ 50 chars each) - * so the filesystem NAME_MAX (255) is not exceeded, then call mkdir_p - * via system(), then ray_splay_save → snprintf("%s/.d") fires range. + * so the filesystem NAME_MAX (255) is not exceeded, then create it + * with ray_test_mkdir_p, then ray_splay_save → snprintf("%s/.d") fires range. * * Path layout (each component 50 chars): * /tmp/rft_deep_save/ (18 chars) @@ -748,6 +742,10 @@ static test_result_t test_save_dir_path_too_long(void) { * fires under the same condition on Linux PATH_MAX = 4096. Skip * on Darwin — the Linux runner covers the regression. */ SKIP("PATH_MAX=1024 on macOS — deep-mkdir fixture not portable"); +#elif defined(_WIN32) + /* Win32 directory paths stop at MAX_PATH (~260) without long-path + * opt-in, far short of the 1021-char tree. */ + SKIP("MAX_PATH=260 on Windows — deep-mkdir fixture not portable"); #endif /* Construct the nested path in a buffer */ char long_dir[2048]; @@ -771,10 +769,8 @@ static test_result_t test_save_dir_path_too_long(void) { TEST_ASSERT_TRUE((size_t)off >= 1021); /* Create the directory tree so ray_mkdir_p inside save succeeds. - * system("mkdir -p ...") handles arbitrarily deep paths. */ - char mk[4096]; - snprintf(mk, sizeof(mk), "mkdir -p \"%s\"", long_dir); - (void)!system(mk); + * ray_test_mkdir_p handles arbitrarily deep paths. */ + (void)ray_test_mkdir_p(long_dir); int64_t id_v2 = ray_sym_intern("v2long", 6); int64_t raw[] = {1}; @@ -794,9 +790,7 @@ static test_result_t test_save_dir_path_too_long(void) { ray_release(col); ray_release(tbl); /* Cleanup entire nested tree from the base */ - char rm_cmd[256]; - snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf /tmp/rft_deep_save"); - (void)!system(rm_cmd); + (void)ray_test_rm_rf("/tmp/rft_deep_save"); PASS(); } @@ -884,9 +878,7 @@ static test_result_t test_trace_missing_schema(void) { const char* dir = TMP_SPLAY_BASE "/trace_noschema"; rm_rf(dir); /* Create dir without .d file */ - char mk[512]; - snprintf(mk, sizeof(mk), "mkdir -p %s", dir); - (void)!system(mk); + (void)ray_test_mkdir_p(dir); setenv("RAY_CSV_TRACE", "1", 1); ray_t* r = ray_splay_load(dir, NULL); @@ -985,11 +977,14 @@ static test_result_t test_trace_fresh_load(void) { * (with .d last, the column save is the first write to hit the dir). * ========================================================================= */ static test_result_t test_save_schema_write_fails(void) { +#if defined(_WIN32) + /* chmod cannot make a Windows directory refuse new files: the + * read-only attribute is ignored for directories. */ + SKIP("read-only directories are not enforced on Windows"); +#endif const char* dir = TMP_SPLAY_BASE "/no_write_schema"; rm_rf(dir); - char mk[512]; - snprintf(mk, sizeof(mk), "mkdir -p %s", dir); - (void)!system(mk); + (void)ray_test_mkdir_p(dir); /* Make dir read-only so .d cannot be written */ chmod(dir, 0555); diff --git a/test/test_store.c b/test/test_store.c index f4ca10561..1e7d849a7 100644 --- a/test/test_store.c +++ b/test/test_store.c @@ -326,7 +326,7 @@ static test_result_t test_col_mmap_nofile(void) { static test_result_t test_splay_open_roundtrip(void) { /* Clean up any leftover splay dir */ - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); /* Build a 3-column table: I64, F64, I32 */ ray_t* tbl = ray_table_new(4); @@ -398,14 +398,14 @@ static test_result_t test_splay_open_roundtrip(void) { ray_release(tbl); /* Cleanup */ - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* ---- test_splay_str_column_roundtrip ----------------------------------- */ static test_result_t test_splay_str_column_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); ray_t* tbl = ray_table_new(2); TEST_ASSERT_NOT_NULL(tbl); @@ -471,7 +471,7 @@ static test_result_t test_splay_str_column_roundtrip(void) { ray_release(names); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } @@ -485,7 +485,7 @@ static test_result_t test_splay_str_column_roundtrip(void) { * ---------------------------------------------------------------------- */ static test_result_t test_splay_short_strv_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); int64_t id_short = ray_sym_intern("short", 5); int64_t id_empty = ray_sym_intern("empty", 5); @@ -547,13 +547,13 @@ static test_result_t test_splay_short_strv_roundtrip(void) { ray_release(tbl); ray_release(tbl2); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* ---- test_splay_dict_column_roundtrip --------------------------------- */ static test_result_t test_splay_dict_column_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); int64_t ids_raw[] = {1, 2}; ray_t* ids = ray_vec_from_raw(RAY_I64, ids_raw, 2); @@ -608,13 +608,13 @@ static test_result_t test_splay_dict_column_roundtrip(void) { ray_release(tbl); ray_release(ids); ray_release(sched); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* ---- test_splay_empty_list_column_roundtrip --------------------------- */ static test_result_t test_splay_empty_list_column_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); ray_t* ids = ray_vec_new(RAY_I64, 0); ray_t* who = ray_vec_new(RAY_SYM, 0); @@ -657,14 +657,14 @@ static test_result_t test_splay_empty_list_column_roundtrip(void) { ray_release(ids); ray_release(who); ray_release(sched); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* A deterministic unsupported column must be rejected before an earlier * column can replace the committed generation. */ static test_result_t test_splay_save_preflight_preserves_generation(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); int64_t k_id = ray_sym_intern("k", 1); int64_t v_id = ray_sym_intern("v", 1); @@ -705,7 +705,7 @@ static test_result_t test_splay_save_preflight_preserves_generation(void) { ray_release(good); ray_release(old_v); ray_release(old_k); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } @@ -838,9 +838,9 @@ static test_result_t test_parted_release(void) { static test_result_t test_part_open(void) { /* Setup: create a 2-partition db with 2 columns each */ - (void)!system("rm -rf " TMP_PART_DB); - (void)!system("mkdir -p " TMP_PART_DB "/2024.01.01/" TMP_TABLE_NAME); - (void)!system("mkdir -p " TMP_PART_DB "/2024.01.02/" TMP_TABLE_NAME); + (void)ray_test_rm_rf(TMP_PART_DB); + (void)ray_test_mkdir_p(TMP_PART_DB "/2024.01.01/" TMP_TABLE_NAME); + (void)ray_test_mkdir_p(TMP_PART_DB "/2024.01.02/" TMP_TABLE_NAME); /* Partition 1: 3 rows */ int64_t raw_a1[] = {10, 20, 30}; @@ -941,7 +941,7 @@ static test_result_t test_part_open(void) { /* Release — should unmap all segments */ ray_release(parted); - (void)!system("rm -rf " TMP_PART_DB); + (void)ray_test_rm_rf(TMP_PART_DB); PASS(); } @@ -949,7 +949,7 @@ static test_result_t test_part_open(void) { /* ray_parted_tables lists the splayed-table subdirectories of the first * partition as a sorted SYM vector usable with ray_read_parted. */ static test_result_t test_parted_tables(void) { - (void)!system("rm -rf " TMP_PART_DB); + (void)ray_test_rm_rf(TMP_PART_DB); /* Two tables (trades, quotes) across two partitions. */ const char* dirs[] = { TMP_PART_DB "/2024.01.01/trades", TMP_PART_DB "/2024.01.01/quotes", @@ -986,16 +986,17 @@ static test_result_t test_parted_tables(void) { /* An existing-but-empty root (no partition dirs) lists no tables — * an empty SYM vector, not an error. */ - (void)!system("rm -rf " TMP_PART_DB "_np && mkdir -p " TMP_PART_DB "_np"); + (void)ray_test_rm_rf(TMP_PART_DB "_np"); + (void)ray_test_mkdir_p(TMP_PART_DB "_np"); ray_t* empty = ray_parted_tables(TMP_PART_DB "_np"); TEST_ASSERT_NOT_NULL(empty); TEST_ASSERT_FALSE(RAY_IS_ERR(empty)); TEST_ASSERT_EQ_I(empty->type, RAY_SYM); TEST_ASSERT_EQ_I(empty->len, 0); ray_release(empty); - (void)!system("rm -rf " TMP_PART_DB "_np"); + (void)ray_test_rm_rf(TMP_PART_DB "_np"); - (void)!system("rm -rf " TMP_PART_DB); + (void)ray_test_rm_rf(TMP_PART_DB); PASS(); } @@ -1662,7 +1663,7 @@ static test_result_t test_sym_col_valid_roundtrip(void) { #define TMP_SYM_PATH "/tmp/rayforce_test_splay_sym_file" static test_result_t test_splay_load_with_sym(void) { - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); /* Intern symbols and build a table with a RAY_SYM column */ @@ -1711,7 +1712,7 @@ static test_result_t test_splay_load_with_sym(void) { ray_release(col_name); ray_release(col_age); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); unlink(TMP_SYM_PATH ".lk"); PASS(); @@ -1720,7 +1721,7 @@ static test_result_t test_splay_load_with_sym(void) { /* ---- test_splay_load_sym_missing_corrupt ------------------------------- */ static test_result_t test_splay_load_sym_missing_corrupt(void) { - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); /* Intern symbols and build a table with a RAY_SYM column */ @@ -1757,7 +1758,7 @@ static test_result_t test_splay_load_sym_missing_corrupt(void) { ray_release(col); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); unlink(TMP_SYM_PATH ".lk"); PASS(); @@ -1766,7 +1767,7 @@ static test_result_t test_splay_load_sym_missing_corrupt(void) { /* ---- test_read_splayed_bad_sym_fatal ----------------------------------- */ static test_result_t test_read_splayed_bad_sym_fatal(void) { - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); /* Build a simple table (no RAY_SYM columns needed) */ int64_t id_x = ray_sym_intern("x", 1); @@ -1793,7 +1794,7 @@ static test_result_t test_read_splayed_bad_sym_fatal(void) { ray_release(col_x); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); PASS(); } diff --git a/test/test_term.c b/test/test_term.c index f29ddc086..319c2f983 100644 --- a/test/test_term.c +++ b/test/test_term.c @@ -1185,7 +1185,12 @@ static test_result_t test_term_prompt_emits_bytes(void) { ray_term_prompt(t); fflush(stdout); int32_t n = capture_end(saved, path, cap, sizeof cap); - int saw_arrow = strstr(cap, "\xe2\x80\xa3") != NULL; /* ‣ */ + /* ‣ (U+2023), or ► (U+25BA) where no console font has ‣ — see term.c */ +#if defined(RAY_OS_WINDOWS) + int saw_arrow = strstr(cap, "\xe2\x96\xba") != NULL; +#else + int saw_arrow = strstr(cap, "\xe2\x80\xa3") != NULL; +#endif int saw_green = strstr(cap, "\033[32m") != NULL; TEST_ASSERT_FMT(n > 0, "no prompt output"); TEST_ASSERT_FMT(saw_arrow, "missing ‣ arrow in prompt: bytes=%d", n); diff --git a/test/test_traverse.c b/test/test_traverse.c index db72da8da..d976d0d4b 100644 --- a/test/test_traverse.c +++ b/test/test_traverse.c @@ -33,7 +33,7 @@ #include #include #include -#ifndef __SANITIZE_ADDRESS__ +#if !defined(__SANITIZE_ADDRESS__) && !defined(_WIN32) /* setrlimit: POSIX only */ #include #endif @@ -3904,7 +3904,7 @@ static test_result_t test_k_shortest_found_path_dup(void) { /* -------------------------------------------------------------------------- * Helper: read VmSize from /proc/self/status; returns 0 on failure. * -------------------------------------------------------------------------- */ -#ifndef __SANITIZE_ADDRESS__ +#if !defined(__SANITIZE_ADDRESS__) && !defined(_WIN32) #include static size_t get_vmsize_bytes(void) { FILE* f = fopen("/proc/self/status", "r"); @@ -4241,7 +4241,7 @@ static test_result_t test_traverse_oom_paths(void) { ray_heap_destroy(); PASS(); } -#endif /* __SANITIZE_ADDRESS__ */ +#endif /* !__SANITIZE_ADDRESS__ && !_WIN32 */ /* -------------------------------------------------------------------------- * Test: exec_expand with SIP bitmap build where rev.n_nodes > fwd.n_nodes. @@ -5725,7 +5725,7 @@ const test_entry_t traverse_entries[] = { { "traverse/k_shortest_large_k", test_k_shortest_large_k, NULL, NULL }, { "traverse/betweenness_with_rev_edges", test_betweenness_with_rev_edges, NULL, NULL }, { "traverse/closeness_sampled_norm", test_closeness_sampled_norm, NULL, NULL }, -#ifndef __SANITIZE_ADDRESS__ +#if !defined(__SANITIZE_ADDRESS__) && !defined(_WIN32) { "traverse/traverse_oom_paths", test_traverse_oom_paths, NULL, NULL }, #endif { "traverse/shortest_path_exceeds_254", test_shortest_path_exceeds_254, NULL, NULL },