Skip to content

About

离线校验 Lua 技能:本机没有 Lua 解释器也能跑(用 lupa 把 Lua 塞进 Python)——语法编译检查 + 桩件与虚拟时钟驱动状态机做行为断言。适用于没有 Source/、打不开编辑器的 UE/UnLua 项目。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lua-offline-verify

在没有 Lua 解释器的机器上离线校验 Lua 代码——把 Lua 塞进 Python 里,用桩件和虚拟时钟 把状态机真跑起来。

场景:改了 UE / UnLua 项目的 Lua,但本机没有 Source/、开不了编辑器、编译不了; 代码又靠 Timer 轮询驱动,超时分支要等几十秒。这套方法能在几毫秒内把逻辑跑完并断言。

这是一个 Agent Skill:SKILL.md 描述完整方法论, examples/ 里是可直接运行的示例和两个工具脚本。

两层

  1. 语法编译检查 —— lupa 的 lua.compile(src) 只编译不执行, 不需要任何依赖就能查出语法错误,10 秒挡住低级错。
  2. 桩件 + 虚拟时钟 —— 把 Lua 侧所有外部依赖换成桩件,再手动驱动 tick, 真跑状态机并断言行为。

「能编译」和「逻辑对」是两件事,所以两层都要。

六个必踩的坑

1. 不加虚拟时钟,超时分支永远测不到。

deadline = os.time() + 15 在离线自测里永远不会到点——几十次 tick 在几毫秒内跑完, 看起来"用例卡住了",其实只是时间没走。把 os.time 替换成手动递增的变量即可。

2. Timer:AddRepeatTimer(nil, 1, fn) 实际传了 4 个参数。

冒号语法糖会把 Timer 自身算作第一个参数。按 function(_, _, fn) 接,fn 拿到的会是 间隔 1,报错是 attempt to call a number value。用「取最后一个参数 + 类型断言」最稳: 真实签名变了会立刻报错,而不是留到测试跑出莫名结果。

3. 每条用例前必须 reset 定时器。

上一条失败用例留下的 Timer 会继续在后续测试里 tick,导致"莫名多出一次 Finish 调用" 这种假失败,排查很费时间。顺带建议每条用例收尾断言 TimerCount() == 0 —— 专抓"异常分支忘了 StopTimer",这类泄漏正常路径完全看不出来。

4. 桩件必须在 loadfile 被测模块之前塞进 package.loaded。

否则被测模块里的 require 会直接失败。

5. 直接按 encoding='utf-8' 读文件,会把好文件判成语法错。

文件形态 后果
UTF-8 带 BOM 编译失败 unexpected symbol near '<\239>'(文件没问题,是读法的问题)
GBK / UTF-16 脚本直接 UnicodeDecodeError 崩掉,整个门禁挂死
带 #! shebang 编译失败 unexpected symbol near '#'(lua.compile 不跳 shebang 行,只有 loadfile 会跳)

examples/read_lua.py 是处理好的版本,可直接复用。

6. lupa 是 Lua 5.5,比 UE 项目常用的 5.4 更严格。

5.5 把 for 循环变量变成了只读(const),所以 for k, v in pairs(t) do k = f(k) end 这种 5.4 里完全合法的写法会直接编译失败:

attempt to assign to const variable 'k'

这是假阳性,别直接当语法错报给同事。 反过来也要留神:「编译通过」只证明符合 Lua 5.5, 不证明符合你项目实际用的那个版本。汇报口径写「按 Lua 5.5 语法通过」最稳。

快速开始

python -m venv .venv
.venv/Scripts/python.exe -m pip install lupa        # macOS / Linux 用 .venv/bin/python

python examples/run_selftest.py                     # → 合计 18 通过 / 0 失败
python examples/read_lua.py <你的 Lua 目录>          # 全量语法门禁(退出码可直接接 CI)
python examples/scan_project.py <你的 Lua 目录>      # 分档体检,看哪些值得写测试
examples/
├── task_runner.lua      # 被测模块:轮询异步任务,带超时 / 防重入 / 失败降级
├── _selftest.lua        # 18 条断言,5 个用例(正常完成 / 超时 / 接口报错 / 防重入+Abort / 抛异常降级)
├── run_selftest.py      # 用 lupa 跑 _selftest.lua,退出码 0/1,可直接接进 CI
├── read_lua.py          # 第一层稳健读法(编码 / BOM / shebang)+ 语法门禁 CLI
└── scan_project.py      # 全项目扫描分档:语法门禁 + 第二层适配度排行

把 task_runner.lua 换成你自己的模块、_selftest.lua 换成你的桩件与断言即可。

先扫全项目分档,再决定给谁写测试

一个项目几十上百个 Lua,别平均用力。scan_project.py 一次给出两件事:

  • 全量语法门禁 —— 几百个文件一起过一遍 lua.compile,适合放在提交前兜底 (注意上面第 6 条的 5.5 假阳性)
  • 第二层适配度排行 —— 按 Timer / os.time / require / 分支数 / 行数 打分

实测一个真实 UE 项目(数百个 Lua、十几万行):约三分之一属于「第二层基本用不上」 (纯配置表、薄绑定层、只做转发的组件),而分数最高的那批集中在自动化测试套件上。 这个结论本身就有用——它告诉你精力该压在哪三十来个文件上。

说清楚边界

能证明 不能证明
语法正确 真实控件名 / 面板名对不对
状态机的推进、超时、失败、幂等 引擎 API 的真实行为(UnLua 绑定、UMG 布局)
上报 / 日志的调用时序 真实时间下的表现(虚拟时钟是加速的)
参数解析、防重入、守卫分支 需要真机 / 编辑器的副作用(截图、录屏、网络)

建议结论口径:「状态机逻辑已离线验证,控件名仍需在引擎里对齐」, 不要说成"已验证通过"。这条纪律比技术本身更重要。

还有一类硬边界:如果模块用 os.clock() 或引擎自己的时间源取时间(不是 os.time()), 虚拟时钟拦不住,超时分支离线永远跑不到——此时退回第一层,或在交付说明里老实写 「超时分支未验证」。

让代码可被这样测

写成可测的样子成本很低,SKILL.md 第七节列了五条,关键在于:

  • 依赖通过 require 引入,不要直接摸全局(这样能用 package.loaded 换桩)
  • 计时统一走 os.time()(虚拟时钟可拦),不要用 os.clock()
  • 周期性逻辑挂在可替换的 Timer 上,不要自己 while true
  • 每个"外部世界"的读操作都包 pcall,桩件返回 nil 时走日志分支而不是抛错

许可

MIT,见 LICENSE。

About

离线校验 Lua 技能:本机没有 Lua 解释器也能跑(用 lupa 把 Lua 塞进 Python)——语法编译检查 + 桩件与虚拟时钟驱动状态机做行为断言。适用于没有 Source/、打不开编辑器的 UE/UnLua 项目。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors