diff --git a/tests/engine/gate_meta_coverage.lua b/tests/engine/gate_meta_coverage.lua index 0646247d..2c29d47c 100644 --- a/tests/engine/gate_meta_coverage.lua +++ b/tests/engine/gate_meta_coverage.lua @@ -63,13 +63,17 @@ end -- engine tier, the SDK cases, and any tests a shipped mod carries local function testCorpus() local files, bodies = {}, {} - local pipe = io.popen( - "ls tests/*.lua tests/engine/*.lua tests/modkit/cases/*.lua mods/*/tests/*.lua 2>/dev/null") - if pipe then - for line in pipe:lines() do - if line ~= "" then files[#files + 1] = line end + local FsIo = require("tests.fs_io") + local function addLuaFrom(dir) + for _, name in ipairs(FsIo.listDir(dir)) do + if name:match("%.lua$") then files[#files + 1] = dir .. "/" .. name end end - pipe:close() + end + addLuaFrom("tests") + addLuaFrom("tests/engine") + addLuaFrom("tests/modkit/cases") + for _, mod in ipairs(FsIo.listDir("mods")) do + if not mod:find(".", 1, true) then addLuaFrom("mods/" .. mod .. "/tests") end end for _, path in ipairs(files) do bodies[path] = slurp(path) or "" diff --git a/tests/fs_io.lua b/tests/fs_io.lua index 6a1d892e..701189d2 100644 --- a/tests/fs_io.lua +++ b/tests/fs_io.lua @@ -15,6 +15,71 @@ local function quote(path) return "'" .. tostring(path):gsub("'", "'\\''") .. "'" end +-- The suites also run on Windows checkouts, where cmd has no ls/find/test. +-- These probes pick the spelling the host shell understands; anything that +-- listed a directory through a bare Unix command silently returned nothing +-- there, and a tier built on an empty listing passes vacuously. +FsIo.isWindows = package.config:sub(1, 1) == "\\" + +local function shellLines(cmd) + local lines = {} + local pipe = io.popen(cmd) + if not pipe then return lines end + for line in pipe:lines() do + if line ~= "" then lines[#lines + 1] = line end + end + pipe:close() + return lines +end + +-- names directly inside path (files and directories mixed, like ls -1) +function FsIo.listDir(path) + local cmd + if FsIo.isWindows then + cmd = 'dir /b "' .. tostring(path) .. '" 2>nul' + else + cmd = "ls -1 " .. quote(path) .. " 2>/dev/null" + end + local items = shellLines(cmd) + table.sort(items) + return items +end + +-- every *.lua under dir, recursively, as forward-slash paths +function FsIo.luaFilesUnder(dir) + local cmd + if FsIo.isWindows then + cmd = 'dir /b /s "' .. tostring(dir) .. '\\*.lua" 2>nul' + else + -- -L follows symlinks: a checkout that symlinks src/ (worktrees, the + -- ROM-free CI probe) would otherwise scan nothing and hand every gate + -- an empty catalog to pass vacuously against + cmd = "find -L " .. quote(dir) .. " -name '*.lua' -type f 2>/dev/null" + end + local files = {} + for _, line in ipairs(shellLines(cmd)) do + files[#files + 1] = (line:gsub("\\", "/")) + end + table.sort(files) + return files +end + +-- existence probe that never shells out on Windows: directories do not +-- open() there at all, and rename-self succeeds for anything that exists +function FsIo.isDir(path) + local handle = io.open(path, "rb") + if handle then + local probe = handle:read(1) + handle:close() + if probe ~= nil then return false end + if FsIo.isWindows then return false end -- opened but empty: a file + elseif FsIo.isWindows then + return os.rename(path, path) == true + end + local ok = os.execute("test -d " .. quote(path)) + return ok == true or ok == 0 +end + function FsIo.new(rootDir) local base = rootDir or "." @@ -50,8 +115,7 @@ function FsIo.new(rootDir) -- a directory opens on some libc builds but reads nothing if probe ~= nil then return { type = "file" } end end - local ok = os.execute("test -d " .. quote(abs(path))) - if ok == true or ok == 0 then return { type = "directory" } end + if FsIo.isDir(abs(path)) then return { type = "directory" } end if handle then return { type = "file" } end return nil end @@ -61,15 +125,7 @@ function FsIo.new(rootDir) end function fs.getDirectoryItems(path) - local items = {} - local pipe = io.popen("ls -1 " .. quote(abs(path)) .. " 2>/dev/null") - if not pipe then return items end - for line in pipe:lines() do - if line ~= "" then items[#items + 1] = line end - end - pipe:close() - table.sort(items) - return items + return FsIo.listDir(abs(path)) end fs.root = base diff --git a/tests/modkit/catalog.lua b/tests/modkit/catalog.lua index 5963ae4d..5fce4fd9 100644 --- a/tests/modkit/catalog.lua +++ b/tests/modkit/catalog.lua @@ -10,22 +10,12 @@ -- call site exists. local Schemas = require("src.mods.Schemas") +local FsIo = require("tests.fs_io") local Catalog = {} local function luaFilesUnder(dir) - local files = {} - -- -L follows symlinks: a checkout that symlinks src/ (worktrees, the - -- ROM-free CI probe) would otherwise scan nothing and hand every gate an - -- empty catalog to pass vacuously against - local pipe = io.popen("find -L " .. dir .. " -name '*.lua' -type f 2>/dev/null") - if not pipe then return files end - for line in pipe:lines() do - if line ~= "" then files[#files + 1] = line end - end - pipe:close() - table.sort(files) - return files + return FsIo.luaFilesUnder(dir) end local function scan(dirs, patterns) diff --git a/tests/run_modkit.lua b/tests/run_modkit.lua index b1814ac2..0849f3ce 100644 --- a/tests/run_modkit.lua +++ b/tests/run_modkit.lua @@ -11,12 +11,12 @@ local dirs = { "tests/modkit/cases" } -- mods ship their own tests (21-testing-and-ci "how mods ship their own -- tests"); pick up every mods//tests directory that exists -local pipe = io.popen("ls -d mods/*/tests 2>/dev/null") -if pipe then - for line in pipe:lines() do - if line ~= "" then dirs[#dirs + 1] = line end +local FsIo = require("tests.fs_io") +for _, name in ipairs(FsIo.listDir("mods")) do + if not name:find(".", 1, true) then + local dir = "mods/" .. name .. "/tests" + if FsIo.isDir(dir) then dirs[#dirs + 1] = dir end end - pipe:close() end Runner.main(dirs, "modkit") diff --git a/tests/tier_runner.lua b/tests/tier_runner.lua index 02ea8b0f..724ee7eb 100644 --- a/tests/tier_runner.lua +++ b/tests/tier_runner.lua @@ -15,6 +15,8 @@ local Runner = {} +local FsIo = require("tests.fs_io") + local function interpreter() -- arg[-1] is how the suite was invoked (luajit here, lua5.4 elsewhere) return (arg and arg[-1]) or "luajit" @@ -22,17 +24,13 @@ end function Runner.suites(dir) local files = {} - local pipe = io.popen(("ls -1 '%s'/*.lua 2>/dev/null"):format(dir)) - if not pipe then return files end - for line in pipe:lines() do - local name = line:match("[^/]+$") + for _, name in ipairs(FsIo.listDir(dir)) do -- "_" prefixes helpers; facts.lua is the tier's pinned-value table -- (a content_/facts.lua is data the suites read, not a suite) - if name and name:sub(1, 1) ~= "_" and name ~= "facts.lua" then - files[#files + 1] = line + if name:match("%.lua$") and name:sub(1, 1) ~= "_" and name ~= "facts.lua" then + files[#files + 1] = dir .. "/" .. name end end - pipe:close() table.sort(files) return files end