「4lua 系列」:acceptance4lua · crap4lua · dry4lua · mutate4lua
mutate4lua 是一个纯 Lua、与宿主无关的变异测试库,适用于 Lua 项目。实现代码位于
src/ 目录下;宿主项目将其 vendor 进来,通过
require("mutate4lua.cli").run(args, env) 暴露 CLI。
- 归因矩阵取代独立覆盖率 baseline:覆盖过滤与测试子集化复用同一份 per-file 行归因, baseline 只负责确认测试套件通过及提供超时基准。
- scanner 在产点时抑制构造性等价变异;被抑制位点不进入执行集或 mutation score 分母,报告会显示抑制数量。
--reuse-coverage保留原有 CLI 表面,但语义改为要求复用新鲜的归因矩阵缓存,不再读取luacov.report.out。
mutate4lua 遵循"Lua 忠实实现上游规格"的总纲,以
mutate4java 的 spec.md 为参照(上游三版中
最成熟者):CLI 表面、选项默认值、位点选择语义、报告骨架及退出码均向其收敛,零本地
独有发明。跨仓库决策以 ADR 形式记录于 luatools notes 仓库
(projects/luatools/docs/adr/)。
对齐不变量 (Aligned Invariants)
- 退出码
0成功 /1用法错误 /2baseline 失败 /3存在存活 mutant (java 约定——clj/go 版仅有 0/1)。 - manifest 以
--[[ ]]footer 注释形式嵌入目标文件尾部(ADR-0002;外部.mutate4lua/manifest/存储已废除,提供一次性迁移脚本处理现有存储)。 - 差异化选择:无 manifest → 所有已覆盖位点;模块 hash 未变 → 0 个位点;hash 变化 → 仅变动的 scope。manifest 仅在干净运行后写入(无 survivor,或无 mutant 需执行)—— 本地"90% 击杀率自动记录"规则已废除(ADR-0004)。
- 选项表面 = 上游集合:
--scan、--update-manifest、--lines、--since-last-run、--mutate-all、--mutation-warning(50)、--max-workers(cores/2)、--timeout-factor(10,非旧版本地值 15)、--test-command、--reuse-coverage、--verbose。已删除的本地独有选项:--index-suites、--lane、--runner、--dry-run、--poll-interval、--json(ADR-0004;--migrate-manifest改为一次性迁移脚本,非 CLI 标志)。 - 变异集合:布尔翻转、比较/算术运算符交换、
and↔or、一元not/-移除、0↔1、右值 →nil(java 版右值 →null的 Lua 对应)。 - scope 身份以声明为基准,非行号为基准(java 规格 §7.3)。
- worker 操作模块副本;原始文件永不在原地变异;超时的 mutant 视为已击杀。
- 测试经内建 luaunit runner(
src/runner.lua)运行——上游 "内置构建工具调用"(maven /go test)的 Lua 对应;宿主测试遵循test_*.lua约定(ADR-0005)。--test-command为上游同款逃生舱。
有意偏离 (Deliberate Deviations)
- 覆盖率来源仅限于 luacov 产物(ADR-0003)——JaCoCo/coverprofile 的 Lua 对应方案;测试框架限定 luaunit(宿主生态已统一,ADR-0005)。
- scope 语义 hash = 对归一化 AST 序列化结果取 FNV-1a(格式无关,clj 的
pr-str语义升级为 AST;上游三版不一——clj 格式无关 / go 空白归一化 / java 原始子串—— 因此选择符合 AST 迁移的语义,ADR-0001/0004)。
本分支的破坏性变更:suites/--lane、--json、外部 manifest 存储、90% 自动
记录规则及临时 driver 覆盖率均已移除;driver 契约(env.default_driver)已删除,
测试改经内建 luaunit runner 运行(ADR-0005)。升级路径见"迁移"一节。
- 接受单个 Lua 源文件作为目标。
- 使用 luacheck AST 扫描器发现变异位点。
- 按声明 scope 追踪差异化变异:默认情况下,仅重测自记录 manifest 以来 semantic
hash 发生变化的 scope;
--mutate-all强制执行所有已覆盖位点。 - 将 manifest 存储为目标源文件末尾的嵌入式
--[[ mutate4lua-manifest ... ]]footer 注释。 - 仅在完全干净运行后写入 manifest(所有已执行 mutant 均被击杀,或无 mutant 需
执行),存在 survivor 或 baseline 失败后绝不写入;
--lines抑制写入,因其记录的 是部分状态。 - 在隔离的工作区副本中运行测试,跨 worker 并行执行(默认:CPU 核数的一半;
--max-workers 1为串行)。 - 使用标准
luacov.report.out中的行覆盖率跳过未覆盖位点;--reuse-coverage跳过重新生成,使用--test-command则禁用覆盖率过滤并给出警告。 - 以文本形式报告已击杀、存活及超时的 mutant。
变异集合包括布尔字面量(true ↔ false)、比较运算符与算术运算符、布尔运算符
(and ↔ or)、一元 not 与一元负号移除、整型常量 0 ↔ 1、字符串字面量
(→ nil)以及函数调用表达式(→ nil)。
本包仅暴露 Lua 模块。不提供独立的 bin/ 入口点;宿主通过薄 wrapper 接入:
package.path = "vendor/mutate4lua/src/?.lua;vendor/mutate4lua/src/?/init.lua;" .. package.path
os.exit(require("mutate4lua.cli").run(arg, {
cwd = ".",
command_name = "lua tools/mutate.lua",
}))通过此类 wrapper 的典型用法:
lua tools/mutate.lua --help
lua tools/mutate.lua src/demo/flag.lua
lua tools/mutate.lua src/demo/flag.lua --scan
lua tools/mutate.lua src/demo/flag.lua --lines 12,18
lua tools/mutate.lua src/demo/flag.lua --since-last-run
lua tools/mutate.lua src/demo/flag.lua --mutate-all
lua tools/mutate.lua src/demo/flag.lua --update-manifest
lua tools/mutate.lua src/demo/flag.lua --reuse-coverage
lua tools/mutate.lua src/demo/flag.lua --test-command "busted"子命令与选项(对应 --help 输出):
| Flag | 含义 |
|---|---|
| (default) | 运行变异测试 |
--scan |
列出变异位点,不运行测试 |
--update-manifest |
写入/刷新嵌入式 manifest footer,不执行变异 |
--mutate-all |
变异所有已覆盖位点,忽略 manifest |
--since-last-run |
仅变异与 manifest 不同的 scope |
--lines N,N |
限定指定行号(抑制 manifest 写入) |
--reuse-coverage |
复用已有的 luacov.report.out,不重新生成 |
--max-workers N |
并行 worker 数(默认:CPU 核数的一半;1 = 串行) |
--timeout-factor N |
mutant 超时倍数,基于 baseline 时长(默认 10) |
--test-command CMD |
自定义测试命令,绕过内建 runner(禁用覆盖率过滤) |
--mutation-warning N |
当选定位点数超过 N 时警告(默认 50) |
--verbose |
详细输出 |
-h, --help |
显示帮助 |
未提供 --test-command 时,引擎调起内建 runner(src/runner.lua)
驱动测试(ADR-0005)。宿主无需提供任何脚本,只需遵循测试约定:
- 测试文件位于项目根的
spec/、test/或tests/目录下,以test_*.lua命名(fixtures/目录视为测试数据,跳过); - 每个测试文件
return一个 luaunit 测试表(方法以test开头); - 被测模块按
<root>/lib/?.lua、<root>/lib/?/init.lua(及src/同型) 可被require。
引擎按以下方式调用 runner(cwd 为隔离工作区):
- baseline 运行(生成覆盖率):
lua <runner> --coverage-report <abs-path> --quiet - baseline 运行(
--mutate-all或--reuse-coverage):lua <runner> --no-coverage --quiet - mutant 运行:
lua <runner> --no-coverage --fail-fast --quiet
退出 0 表示测试通过(mutant 存活);非零表示失败(mutant 被击杀)。引擎对运行
施加自身的超时控制,超时的 mutant 计为已击杀。
runner 在 --coverage-report 给定的绝对路径上产出标准 luacov.report.out。
引擎自行解析该报告,无需依赖 luacov 库。宿主未安装 luacov 时 baseline 失败并
给出明确提示;--no-coverage 运行不受影响。测试框架(luaunit)与覆盖率工具
(luacov)均经 luarocks 提供;不符合上述约定的宿主可使用 --test-command
(该选项禁用覆盖率过滤并给出警告)。
mutate4lua 解析 luacov 的默认 reporter 输出(即使用 luacov 且未配置自定义
reporter 时产生的格式)。内建 runner 负责生成此精确格式。以下关键特征基于真实
luacov 0.17.0 输出验证:
- 段结构:每个源文件出现在以 78 个
=字符(^=+$)组成的分隔行所界定的 独立块中。============================================================================== <luacov 所见的路径> ============================================================================== - 命中次数列:右对齐;宽度随最大命中次数增长(最小 2 字符以容纳
*0)。 该列后跟一个分隔空格,然后是逐字的源码行(含原始缩进)。 - 未命中可执行行:在次数列位置显示
*0(单星号)——无前导空格。示例:*0 return 0。 - 非可执行行(空行、
end、else、注释):无计数前缀——次数列为空(填充 空格),然后是分隔空格,再是源码行。这些行进号累加但不记录为可执行行。 - 摘要段:始终为报告的最后一块,以
Summary作为"路径"名。解析器遇到Summary即停止,其下的覆盖率表不再消费。 - 路径形态:luacov 按
require()时的所见记录路径——可能是相对路径(如src/foo.lua)或绝对路径(如/Users/.../src/foo.lua)。绝对路径会归一化 为工作区相对路径以匹配目标。
runner 需要 luaunit 执行测试、luacov 插桩以生成 luacov.report.out。Lua 5.4
(唯一支持运行时)的安装方法:
luarocks --lua-version=5.4 --lua-dir=/opt/homebrew/opt/lua@5.4 install luaunit luacov使 require("luaunit") / require("luacov") 在 lua5.4 下生效:
export LUA_PATH="/Users/$USER/.luarocks/share/lua/5.4/?.lua;$LUA_PATH"内建 runner 使用 luacov.runner.init() / luacov.runner.save_stats() 插桩
测试运行,并调用 luacov.reporter.report() 生成报告产物。不符合测试约定的
宿主应提供 --test-command(该选项禁用覆盖率过滤并给出警告)。
- manifest 以嵌入式
--[[ mutate4lua-manifest ... ]]footer 注释形式存储在目标 源文件末尾。 - 记录 manifest 版本
4、模块 hash,以及每个 scope 的:id、kind、startLine、endLine、semanticHash。 - 旧的
lastMutation*字段读取时忽略,写入时不再生成。 - 差异化模式跳过
semanticHash未变的 scope。 - manifest 在干净运行(无 survivor / 无 mutant 需执行)后及
--update-manifest时写入。baseline 失败后、存在任何 survivor 后或--scan时绝不写入。--lines抑制写入。 - 对齐前的外部存储
.mutate4lua/manifest/不再读取或写入。对每个目标文件执行 一次tools/migrate_manifest.lua,从旧存储/旧 footer 迁移到新的 v4 footer:lua tools/migrate_manifest.lua src/foo.lua。
- 对每个被变异文件执行一次
lua tools/migrate_manifest.lua <target.lua>。该脚本 会剥离所有旧 footer、删除对应的.mutate4lua/manifest/<relative>.manifest存储文件,并追加新计算的 v4 footer。 - 从脚本和 CI 中移除以下选项:
--index-suites、--lane、--runner、--dry-run、--poll-interval、--json、--migrate-manifest。 - 删除自定义 driver 脚本与 wrapper 中的
env.default_driver(ADR-0005):将 测试迁移到内建 runner 约定(test_*.lua返回 luaunit 测试表,位于spec|test|tests/下);无法迁移的宿主退回--test-command(该选项禁用覆盖率 过滤并给出警告)。 - 安装 luaunit 与 luacov(luarocks,见上文);否则使用
--test-command。 - 默认
--timeout-factor已从15改为10。 - 若
.mutate4lua/manifest/目录在更早的检出中存在,将其加入.gitignore;将 新的 manifest footer 提交到源文件中。
- vendor
mutate4lua(submodule 或直接复制),在薄 wrapper 脚本中将其src/加入package.path,并按上文所示调用cli.run。 - 按测试约定组织测试:
test/test_*.lua,每个文件return一个 luaunit 测试表; 被测模块放在lib/(或src/)下。无需编写任何 runner/driver 脚本。 - 通过 wrapper 运行变异,如
lua tools/mutate.lua src/foo.lua。manifest 在干净 运行后自动记录;lua tools/mutate.lua src/foo.lua --update-manifest可显式 固化一份。 - 将
.mutate4lua/加入.gitignore(缓存及任何残留存储文件);manifest 位于 目标源文件内,应一并提交。
测试套件使用 luaunit,经内建 runner 运行(与宿主同一套约定,dogfood):
lua tests/run.lua # 运行全部 tests/test_*.lua
lua tests/run.lua tests/test_engine.lua # 运行指定文件需先安装 luaunit(scanner 另需 luacheck,见 ADR-0001)。
0:成功,无存活 mutant,无 mutant 需执行,扫描成功,或 manifest 更新成功1:命令行用法错误或基础设施错误2:baseline 测试失败3:至少一个 mutant 存活
- Lua 5.3+ 或 LuaJIT(内容哈希——文件、scope 及项目哈希——使用原生 64 位位运算符)
- luaunit(内建 runner 的测试框架,luarocks 提供;引擎本身无 luaunit 依赖)
- 如需覆盖率过滤,需 luacov(内建 runner 使用;引擎本身无 luacov 依赖)
- 类 POSIX shell 环境,需有
find、mkdir、cp、rm、sleep及kill(并行 模式使用后台作业与wait) - 可选:
timeout或gtimeout(用于 mutant 超时)——无此命令时,内置纯 shell 看门狗提供相同的 124 退出码语义
- manifest 现以 footer 注释形式写入目标文件;外部
.mutate4lua/manifest/存储 不再使用。 - CLI 不再支持
--lane、--runner、--json或 suite 索引。 - driver 契约(
env.default_driver)已删除(ADR-0005);测试经内建 luaunit runner 运行,宿主遵循test_*.lua约定即可。 - 帮助/错误输出中使用的默认
command_name为mutate4lua。
mutate4lua 是纯 Lua、与宿主无关的变异测试库,代码位于 src/。
宿主项目 vendor 本库后,通过 require("mutate4lua.cli").run(args, env) 暴露 CLI。
它支持扫描变异位点、按 manifest 做差异化变异(默认只重测 semantic hash 变化过的
scope,--mutate-all 强制执行全部已覆盖位点)、在隔离工作区并行执行测试(默认
worker 数为 CPU 核数的一半,--max-workers 1 串行)、按 luacov 报告过滤未覆盖位点,
并输出文本报告。manifest 以 --[[ mutate4lua-manifest ... ]] footer 注释形式嵌入
目标源文件末尾,随源码一起提交。
常用命令(经宿主 wrapper):
lua tools/mutate.lua --help
lua tools/mutate.lua src/demo/flag.lua
lua tools/mutate.lua src/demo/flag.lua --scan
lua tools/mutate.lua src/demo/flag.lua --lines 12,18
lua tools/mutate.lua src/demo/flag.lua --since-last-run
lua tools/mutate.lua src/demo/flag.lua --mutate-all
lua tools/mutate.lua src/demo/flag.lua --update-manifest
lua tools/mutate.lua src/demo/flag.lua --reuse-coverage完整选项列表见 --help 或上文 CLI 一节。
manifest 机制:每个目标文件末尾的 footer 注释,内容为 key=value 正文:
version(当前为 4)与 projectHash 头,加上每个 scope 的 id / kind /
startLine / endLine / semanticHash。旧的 lastMutation* 字段读取时忽略、
写入时不再生成。差异化模式跳过 hash 未变的 scope。manifest 仅在干净运行后
(全部击杀或无 mutant 需执行)或 --update-manifest 时写入;baseline 失败、
存在 survivor、或 --scan 时不写入;--lines 时抑制写入。
从旧版本迁移:对每个目标文件执行
lua tools/migrate_manifest.lua src/foo.lua,它会剥离旧 footer、删除对应
.mutate4lua/manifest/<relative>.manifest 外部存储文件,并追加新的 v4 footer。
之后可删除 --index-suites、--lane、--runner、--dry-run、
--poll-interval、--json、--migrate-manifest 等旧选项;删除自定义 driver
脚本和 wrapper 里的 env.default_driver,把测试迁移到内建 runner 约定
(test_*.lua 返回 luaunit 测试表);注意默认 --timeout-factor 已从 15 改为 10;
安装 luaunit 与 luacov(luarocks)。
内建 runner:未传 --test-command 时,引擎调起 src/runner.lua
运行测试(ADR-0005)。宿主无需提供脚本,遵循约定即可:测试位于 spec|test|tests/
下、以 test_*.lua 命名、返回 luaunit 测试表;被测模块放在 lib/(或 src/)。
引擎对 runner 的调用形态:
- 基线(生成覆盖率):
lua <runner> --coverage-report <abs-path> --quiet - 基线(
--mutate-all/--reuse-coverage):lua <runner> --no-coverage --quiet - 变异体运行:
lua <runner> --no-coverage --fail-fast --quiet
在新项目(如 eggy)中接入:vendor 本库 → 写一个调用 cli.run 的薄 wrapper →
按约定放好 test/test_*.lua → 直接运行 lua tools/mutate.lua src/foo.lua。无需
额外配置;manifest 嵌入在源码文件中,建议提交;.mutate4lua/ 目录加入
.gitignore。
测试使用 luaunit,经内建 runner 运行(与宿主同一约定):
lua tests/run.lua # 运行全部 tests/test_*.lua
lua tests/run.lua tests/test_engine.lua # 运行指定文件0:成功、全部击杀、无 mutant 需执行、扫描成功、或 manifest 更新成功1:命令行用法错误或基础设施错误2:baseline 测试失败3:存在存活的 mutant
- Lua 5.3+ 或 LuaJIT(哈希运算使用原生 64 位位运算)
- luaunit(内建 runner 的测试框架,luarocks 提供)
- 如需覆盖率过滤,需安装 luacov(内建 runner 使用;引擎本身不依赖 luacov)
- POSIX 环境(
find、mkdir、cp、rm、sleep、kill) - 可选
timeout/gtimeout;否则内置纯 shell 看门狗提供 124 退出码语义