Make ROM-free test tiers actually run on Windows (#267)

Suite discovery, the extension-point catalog scan, mod test-dir pickup,
and the meta-coverage corpus all shelled out to ls/find/test -d, which do
not exist in cmd.exe. Every listing came back empty on Windows, so tiers
ran 0 suites and still reported ALL TESTS PASSED.

Add portable probes to tests/fs_io.lua (same Unix commands on
Linux/macOS; dir /b and a shell-free rename-self existence check on
Windows) and rewire the four call sites to them. 15/15 engine suites and
2/2 modkit suites now genuinely run and pass on Windows.

Fixes #266

Co-authored-by: johnjohto <johnjohto@users.noreply.github.com>
This commit is contained in:
johnjohto
2026-07-27 07:06:53 -04:00
committed by GitHub
parent 6a27be54de
commit 08e8dfc416
5 changed files with 89 additions and 41 deletions
+10 -6
View File
@@ -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 ""
+67 -11
View File
@@ -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
+2 -12
View File
@@ -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)
+5 -5
View File
@@ -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/<id>/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")
+5 -7
View File
@@ -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_<mod>/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