Skip to content

Commit ca3224c

Browse files
committed
require 深矩阵(57 行):真在磁盘上造模块,走完整条搜索器链
PackageLib 是未覆盖率第二高的类(15%)。之前的闸门只走了搜索器链的「纯 Lua 那一半」—— 在内存里摆 package.preload,从不真的去磁盘找。所以补一片把模块 真写进 /tmp/fkreq 再让 require 去找的矩阵: - 模块返回 nil / false / true / 数字 / 无返回 / 多值(只取第一个) - 模块抛错、模块语法错、模块里自己设 package.loaded - 点号模块名换目录分隔符、第二次 require 的缓存 - 循环 require:参考给 C stack overflow,不是哨兵 - package.loaded 预置成值 / false / nil 的三种后果 - package.preload 给函数 / 返回 nil / 抛错 / 非函数 - searchpath 的 sep / rep 与缺参 - searchers 的个数、逐个调用、整条链被换掉 顺带钉住一件会悄悄坏掉的事:每条 module 'X' not found 都把候选路径整串打印 出来,于是默认 package.path 必须与参考二进制编译进去的 LUA_PATH_DEFAULT 逐字符一致。 57 行第一跑逐字节一致;敏感性照旧量过(searchpath 默认 sep "." → "-", 红在 sp_dots,随后 git checkout 还原)。 全量 847 绿(+1),门禁过,差分 fuzz 绿。
1 parent 524ba45 commit ca3224c

3 files changed

Lines changed: 219 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ files whose output is deterministic (`api`, `bitwise`, `calls`, `closure`,
207207
**byte for byte**. Five more (`cstack`, `gc`, `literals`, `locals`,
208208
`constructs`) print things that vary with the implementation or with a random
209209
seed, so they are only asserted to run to completion and print `OK` — the reason
210-
for each is listed under *Phase 10* below. 846 tests green, JaCoCo line coverage
210+
for each is listed under *Phase 10* below. 847 tests green, JaCoCo line coverage
211211
0.90 against a 0.89 gate. (The upstream-suite gate needs `LUA54_TESTS` or
212212
`target/lua54-tests`; without a test directory that whole class **skips itself**,
213213
so a green run on a machine without it is 25 tests weaker than it looks.)
@@ -2107,6 +2107,30 @@ libc and there is nothing sensible to model. The difference is that here the
21072107
*observable* is not just an errno but a file offset — which is why they are written
21082108
down rather than merely excluded.
21092109

2110+
#### `require` against a real filesystem
2111+
2112+
The `package` gate used to exercise only the pure-Lua half of the searcher chain,
2113+
by populating `package.preload` in memory. `PackageLib` was still the second-
2114+
thinnest class by coverage, so there is now a matrix that writes **real modules to
2115+
disk** and makes `require` find them: 57 lines covering what a module may return
2116+
(nil, false, true, a number, nothing, multiple values), a module that raises, one
2117+
with a syntax error, one that sets `package.loaded` itself, dotted names turning
2118+
into directories, the cache on a second `require`, a circular `require` (the
2119+
reference gives `C stack overflow`, not a sentinel), `package.loaded` pre-set to a
2120+
value / `false` / `nil`, `package.preload` given a function / a function returning
2121+
nil / a raising function / a non-function, `searchpath`'s `sep` and `rep`, and the
2122+
`searchers` chain — its length, each entry called directly, and the whole chain
2123+
replaced.
2124+
2125+
One thing it pins down that would otherwise rot quietly: every
2126+
`module 'X' not found` message prints the **entire** candidate list, so the default
2127+
`package.path` has to match the reference binary's compiled-in `LUA_PATH_DEFAULT`
2128+
character for character.
2129+
2130+
57 lines, byte-identical on the first run — so, again, sensitivity measured rather
2131+
than assumed: changing `searchpath`'s default `sep` from `"."` to `"-"` turns it
2132+
red at `sp_dots`.
2133+
21102134
### What is still missing
21112135

21122136
Nothing in the language or the ten standard libraries, and every upstream file

‎README_CN.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ Lua 参考实现的解析器是手写递归下降。直接搬过来,就去掉
184184
`utf8`、`vararg`、`verybig` 等)与真实 `lua5.4` 的输出**逐字节一致**;
185185
`cstack` / `gc` / `literals` / `locals` / `constructs` 五个因为打印的是随实现或
186186
随随机数而变的东西,只断言「跑完并打印 `OK`」——理由逐个列在 *阶段 10* 一节里。
187-
全量 846 个用例绿,JaCoCo 行覆盖 0.90(门禁 0.89)。(上游套件这道闸门要
187+
全量 847 个用例绿,JaCoCo 行覆盖 0.90(门禁 0.89)。(上游套件这道闸门要
188188
`LUA54_TESTS` 或 `target/lua54-tests`;没有用例目录时<b>整类自动跳过</b>,
189189
所以那种机器上「绿」其实比看起来少 25 个用例。)
190190

@@ -1903,6 +1903,24 @@ bug 就藏在没人测的那一份里。**
19031903
差别在于这里的**可观测量**不只是 errno 而是文件偏移 —— 所以要写下来,而不是
19041904
悄悄排除。
19051905

1906+
#### 让 `require` 真去磁盘上找模块
1907+
1908+
`package` 那道闸门之前只走了搜索器链的「纯 Lua 那一半」(在内存里摆
1909+
`package.preload`),而 `PackageLib` 是未覆盖率第二高的类。所以补一片
1910+
<b>把模块真写进磁盘</b>再让 `require` 去找的矩阵,57 行:模块返回
1911+
nil / false / true / 数字 / 无返回 / 多值(只取第一个)、模块抛错、模块语法错、
1912+
模块里自己设 `package.loaded`、点号模块名换目录分隔符、第二次 `require` 的缓存、
1913+
循环 `require`(参考给 `C stack overflow`,不是哨兵)、`package.loaded` 预置成
1914+
值 / false / nil 三种后果、`package.preload` 给函数 / 返回 nil / 抛错 / 非函数、
1915+
`searchpath` 的 `sep` 与 `rep`、`package.searchers` 的个数与逐个调用、整条链被换掉。
1916+
1917+
顺带钉住一件会悄悄坏掉的事:每条 `module 'X' not found` 都把候选路径<b>整串</b>
1918+
打印出来,所以默认的 `package.path` 必须与参考二进制编译进去的
1919+
`LUA_PATH_DEFAULT` 逐字符一致。
1920+
1921+
57 行第一跑逐字节一致 —— 所以敏感性照旧是量过的:把 `searchpath` 的默认 `sep`
1922+
从 `"."` 改成 `"-"`,立刻红在 `sp_dots`。
1923+
19061924
### 还没补上的
19071925

19081926
语言与十个标准库本身没有缺项,上游套件里能逐字节比的那些也都比过了。剩下的是

‎src/test/java/com/github/esrrhs/fakescript/lua/PackageLibTest.java‎

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ List<DynamicTest> packageScriptsMatchReference() {
4444
add(tests, "preload", PRELOAD);
4545
add(tests, "lua", LUAMOD);
4646
add(tests, "err", ERRORS);
47+
add(tests, "deep", DEEP);
4748
return tests;
4849
}
4950

@@ -287,4 +288,178 @@ private static void add(List<DynamicTest> tests, String tag, String[] srcs) {
287288
package.searchers = { function(n) error("searcher died") end }
288289
print(pcall(require, "fkpk_e5"))
289290
""", };
291+
292+
/**
293+
* {@code require} / {@code package.searchpath} 的<b>深矩阵</b>(57 行):
294+
* 真在磁盘上造一批模块,然后走完整条搜索器链 —— 而不只是在内存里摆
295+
* {@code package.preload}。之前这一块只覆盖了「纯 Lua 那条路」的一半。
296+
* <p>
297+
* 覆盖到的形状:模块返回 nil / false / true / 数字 / 无返回 / 多值(只取第一)、
298+
* 模块抛错、模块语法错、模块里自己设 {@code package.loaded}、
299+
* 点号模块名换目录分隔符、重复 {@code require} 的缓存、循环 {@code require}
300+
* (参考给 {@code C stack overflow},不是哨兵)、
301+
* {@code package.loaded} 预置成值 / false / nil 的三种后果、
302+
* {@code package.preload} 给函数 / 返回 nil / 抛错 / 非函数、
303+
* {@code searchpath} 的 {@code sep} / {@code rep} 与缺参、
304+
* {@code package.searchers} 的个数与逐个调用、整条链被换掉。
305+
* <p>
306+
* 顺带钉住一件容易悄悄坏掉的事:<b>{@code package.path} 的默认值</b>必须与参考
307+
* 二进制编译时那套 {@code LUA_PATH_DEFAULT} 一致 —— 矩阵里每条
308+
* {@code module 'X' not found} 都把 {@code /usr/local/share/lua/5.4/...} 那
309+
* 一串<b>完整打印</b>出来,改一个字符就红。
310+
* <p>
311+
* 敏感性量过:把 {@code searchpath} 的默认 {@code sep} 从 {@code "."} 改成
312+
* {@code "-"},立刻红在 {@code sp_dots}。
313+
* <p>
314+
* 库目录用 {@code /tmp/fkreq} 并在开头 {@code rm -rf} 重建:两边跑在同一台
315+
* 机器上,绝对路径对双方都可见,也不依赖 cwd。
316+
*/
317+
private static final String[] DEEP = {"""
318+
-- require / package.searchpath 深矩阵:真在磁盘上造模块,走完整条搜索器链
319+
local out = {}
320+
local function d(v)
321+
local t = type(v)
322+
if t == "nil" then return "nil" end
323+
if t == "boolean" then return tostring(v) end
324+
if t == "number" then return math.type(v) .. ":" .. tostring(v) end
325+
if t == "string" then return string.format("q(%d)[%s]", #v, (v:gsub("\\n", "\\\\n"))) end
326+
return t
327+
end
328+
local function probe(name, f, ...)
329+
local r = table.pack(pcall(f, ...))
330+
local s = name .. " [" .. r.n .. "]"
331+
for i = 2, r.n do s = s .. " " .. d(r[i]) end
332+
if not r[1] then s = s .. " ERR " .. d(r[2]) end
333+
out[#out + 1] = s
334+
end
335+
local function say(s) out[#out + 1] = s end
336+
337+
local D = "/tmp/fkreq"
338+
local R = D .. "/repo"
339+
os.execute("rm -rf " .. D .. " && mkdir -p " .. R .. "/sub")
340+
local function put(path, body)
341+
local f = assert(io.open(path, "w"))
342+
f:write(body)
343+
f:close()
344+
end
345+
346+
put(R .. "/basic.lua", "return 'BASIC'\\n")
347+
put(R .. "/nonil.lua", "-- 什么都不返回\\n")
348+
put(R .. "/retnil.lua", "return nil\\n")
349+
put(R .. "/retfalse.lua", "return false\\n")
350+
put(R .. "/rettrue.lua", "return true\\n")
351+
put(R .. "/retnum.lua", "return 42\\n")
352+
put(R .. "/retmulti.lua", "return 1, 2, 3\\n")
353+
put(R .. "/err.lua", "error('boom')\\n")
354+
put(R .. "/syntax.lua", "this is not lua\\n")
355+
put(R .. "/prints.lua", "print('SIDE'); return 'P'\\n")
356+
put(R .. "/setsloaded.lua", "package.loaded['setsloaded'] = 'PRESET'; return 'FROMCHUNK'\\n")
357+
put(R .. "/sub/inner.lua", "return 'INNER'\\n")
358+
put(R .. "/counter.lua", "local n = (package.loaded['counter_n'] or 0) + 1\\n"
359+
.. "package.loaded['counter_n'] = n\\nreturn n\\n")
360+
put(R .. "/circ.lua", "return require('circ')\\n")
361+
362+
-- 把仓库路径前置到 package.path,避免依赖 cwd
363+
local oldpath = package.path
364+
package.path = R .. "/?.lua;" .. R .. "/?/init.lua;" .. oldpath
365+
366+
say("path_head " .. tostring(package.path:sub(1, #R + 6)))
367+
368+
probe("r_basic", require, "basic")
369+
probe("r_basic_twice", require, "basic") -- 缓存:第二次不再跑
370+
probe("r_prints", require, "prints") -- 副作用只发生一次
371+
probe("r_prints2", require, "prints")
372+
probe("r_nonil", require, "nonil") -- 无返回 → true
373+
probe("r_retnil", require, "retnil") -- 返回 nil → true
374+
probe("r_retfalse", require, "retfalse") -- 返回 false → false
375+
probe("r_rettrue", require, "rettrue")
376+
probe("r_retnum", require, "retnum")
377+
probe("r_retmulti", require, "retmulti") -- 只取第一个
378+
probe("r_err", require, "err")
379+
probe("r_syntax", require, "syntax")
380+
probe("r_setsloaded", require, "setsloaded") -- chunk 里先设了 loaded
381+
probe("r_setsloaded2", require, "setsloaded")
382+
probe("r_sub", require, "sub.inner") -- 点号换成目录分隔符
383+
probe("r_counter1", require, "counter")
384+
probe("r_counter2", require, "counter")
385+
probe("r_missing", require, "no_such_mod_xyz")
386+
probe("r_missing_dots", require, "a.b.c.nope")
387+
probe("r_circ", require, "circ") -- 循环 require 给的是哨兵
388+
probe("r_noname", require)
389+
probe("r_nilname", require, nil)
390+
probe("r_numname", require, 42)
391+
probe("r_emptyname", require, "")
392+
probe("r_boolname", require, true)
393+
394+
-- package.loaded 预置
395+
package.loaded["preset"] = "PRE"
396+
probe("l_preset", require, "preset")
397+
package.loaded["prefalse"] = false
398+
probe("l_prefalse", require, "prefalse")
399+
package.loaded["prenil"] = nil
400+
probe("l_prenil", require, "prenil")
401+
402+
-- package.preload
403+
package.preload["frompreload"] = function() return "FROM_PRELOAD" end
404+
probe("p_preload", require, "frompreload")
405+
package.preload["preloadnil"] = function() return nil end
406+
probe("p_preloadnil", require, "preloadnil")
407+
package.preload["preloaderr"] = function() error("pe") end
408+
probe("p_preloaderr", require, "preloaderr")
409+
probe("p_preload_extra", function()
410+
package.preload["multi"] = function(n) return "N=" .. tostring(n) end
411+
return require("multi")
412+
end)
413+
probe("p_preload_badtype", function()
414+
package.preload["notfn"] = 42
415+
return require("notfn")
416+
end)
417+
418+
-- searchpath
419+
probe("sp_basic", package.searchpath, "basic", R .. "/?.lua")
420+
probe("sp_missing", package.searchpath, "nope_xyz", R .. "/?.lua")
421+
probe("sp_multi", package.searchpath, "nope_xyz", R .. "/?.lua;" .. R .. "/?/init.lua")
422+
probe("sp_dots", package.searchpath, "sub.inner", R .. "/?.lua")
423+
probe("sp_sep", package.searchpath, "sub.inner", R .. "/?.lua", ".", "/")
424+
probe("sp_rep", package.searchpath, "subXinner", R .. "/?.lua", "X", "/")
425+
probe("sp_rep2", package.searchpath, "sub--inner", R .. "/?.lua", "--", "/")
426+
probe("sp_emptysep", package.searchpath, "basic", R .. "/?.lua", "", "")
427+
probe("sp_nopath", package.searchpath, "basic", "")
428+
probe("sp_nosep", package.searchpath, "basic", R .. "/?.lua")
429+
probe("sp_badname", package.searchpath, 42, R .. "/?.lua")
430+
probe("sp_badpath", package.searchpath, "basic", 42)
431+
probe("sp_noargs", package.searchpath)
432+
probe("sp_abs", package.searchpath, "basic", R .. "/?.lua;" .. R .. "/?.lua")
433+
434+
-- searchers 链本身
435+
probe("s_count", function() return #package.searchers end)
436+
probe("s_types", function()
437+
local r = {}
438+
for i, v in ipairs(package.searchers) do r[#r + 1] = type(v) end
439+
return table.concat(r, ",")
440+
end)
441+
probe("s_call1", function() return package.searchers[1]("basic") end)
442+
probe("s_call4", function() return package.searchers[4]("basic") end)
443+
probe("s_call_missing1", function() return package.searchers[1]("nope_xyz_1") end)
444+
probe("s_call_bad", function() return package.searchers[1](42) end)
445+
probe("s_replace", function()
446+
local old = package.searchers
447+
package.searchers = { function(n) return function() return "REPLACED" end end }
448+
local v = require("anything_xyz")
449+
package.searchers = old
450+
return v
451+
end)
452+
probe("s_empty", function()
453+
local old = package.searchers
454+
package.searchers = {}
455+
local ok, e = pcall(require, "anything_xyz")
456+
package.searchers = old
457+
return ok, e
458+
end)
459+
460+
package.path = oldpath
461+
os.execute("rm -rf " .. D)
462+
print(table.concat(out, "\\n"))
463+
""" };
464+
290465
}

0 commit comments

Comments
 (0)