Lua API 测试用例设计
本文定义 Lua API 契约的测试矩阵。examples/hello_world/ 是用户参考示例,不作为 API 正确性的唯一验收;API 正确性必须由独立测试覆盖。
测试目标:
- 每个
shield.*API 都有成功、失败和边界行为测试。 - coroutine 挂起语义必须通过运行时测试验证。
- 错误返回必须验证
ok=false和Error.code。 - 旧 API 必须有负向测试,确保不会被重新引入。
测试目录建议
tests/lua_api/
├── CMakeLists.txt
├── lua_api_test_harness.cpp
├── scripts/
│ ├── lifecycle_service.lua
│ ├── messaging_service.lua
│ ├── timer_service.lua
│ ├── data_service.lua
│ ├── gateway_service.lua
│ └── legacy_api_service.lua
└── test_*.cppHarness 要求:
- 能启动最小
shield_core+shield_luaruntime。 - 能加载 Lua service module。
- 能模拟 service 间
send/call。 - 能推进 fake clock 或使用可控 timer scheduler。
- 能注入 fake DB/Redis backend。
- 能捕获 Lua log、error、panic 和 dead letter。
LAPI-001 Module Loader
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-001-01 | Lua 文件返回 table | spawn service | spawn 成功 |
| LAPI-001-02 | Lua 文件返回 nil | spawn service | invalid_service_module |
| LAPI-001-03 | Lua 文件语法错误 | spawn service | script_load_failed |
| LAPI-001-04 | module 顶层抛错 | spawn service | script_load_failed |
LAPI-002 Lifecycle
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-002-01 | on_init 无返回值 | spawn | 成功 |
| LAPI-002-02 | on_init 返回 true | spawn | 成功 |
| LAPI-002-03 | on_init 返回 false, err | spawn | init_failed,name rollback |
| LAPI-002-04 | on_init 抛错 | spawn | init_failed,触发 panic 记录 |
| LAPI-002-05 | service exit | shield.exit("normal") | 调用 on_exit("normal") |
| LAPI-002-06 | on_exit 调用 shield.call | exit | 返回/记录 api_not_allowed_in_exit |
| LAPI-002-07 | runtime shutdown | shutdown | 先调用 on_shutdown(ctx),再调用 on_exit("stopping") |
| LAPI-002-08 | on_shutdown 超时 | shutdown | 记录 timeout,继续调用 on_exit 并释放服务 |
| LAPI-002-09 | on_shutdown 调用 shield.call | shutdown drain | deadline 内允许挂起并恢复 |
LAPI-003 Registry
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-003-01 | spawn with name | shield.query(name) | 返回等价 handle |
| LAPI-003-02 | 重复 name | spawn second | name_conflict |
| LAPI-003-03 | init 失败占名 | query name | 不可见 |
| LAPI-003-04 | shield.register 新 name | query | 可见 |
| LAPI-003-05 | service exit | query old names | service_not_found |
| LAPI-003-06 | invalid name | register | invalid_name |
LAPI-004 Message Send
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-004-01 | target by name | send method args | receiver handler 收到业务参数 |
| LAPI-004-02 | target by handle | send | receiver 收到 |
| LAPI-004-03 | target missing | send | false, service_not_found |
| LAPI-004-04 | method missing | send | dead letter 记录 |
| LAPI-004-06 | self-send | send self | 不 reentrant,下一调度点执行 |
LAPI-005 Message Call
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-005-01 | callee returns one value | call | true, value |
| LAPI-005-02 | callee returns false, reason | call | true, false, reason |
| LAPI-005-03 | callee returns trailing nil | call | argc 保留 |
| LAPI-005-04 | callee throws | call | false, handler_error |
| LAPI-005-05 | method missing | call | false, method_not_found |
| LAPI-005-06 | timeout | call_timeout | false, timeout |
| LAPI-005-07 | late response | callee returns after timeout | response discarded |
| LAPI-005-08 | nested call | handler calls another service | runtime thread 不阻塞 |
| LAPI-005-09 | 主线程/module-level 调 shield.call/call_timeout | 非 coroutine 上下文 | false, {code="call_not_allowed_off_coroutine"} |
| LAPI-005-10 | 协程内 shield.call 挂起期间 | 同 service 第二消息 | actor 不阻塞,第二消息被处理 |
| LAPI-005-11 | on_init 内 shield.call | spawn | on_init 协程挂起至响应到达,spawn 成功完成 |
| LAPI-005-12 | timer/fork callback 内 call + sleep | callback 挂起期间 | actor 不阻塞,回调完成后恢复 |
LAPI-005A Lua-driven Spawn / Exit / Panic
tests/lua_api/test_lua_api_spawn.cpp(scripts: spawn_parent.lua、 spawn_child.lua、spawn_fail_child.lua、spawn_in_init_parent.lua、 spawn_panic_service.lua、spawn_watcher.lua、spawn_opts_shape.lua; 其中 spawn_opts_shape.lua 由 SpawnOptsTableWithMixedKeysStillSpawns 使用,覆盖 opts table 混合键形状,无矩阵编号)。
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-005A-01 | parent on_init | shield.spawn(VM 主线程) | 同步路径成功,child published,handle node()==0 |
| LAPI-005A-02 | handler 协程 spawn 慢 init child | spawn 进行中 call parent ping | parent 不阻塞(<400ms),spawn 完成返回正确 handle |
| LAPI-005A-03 | 异步 spawn 持有 name reservation | 同名 spawn | service name already reserved 快速失败,原 spawn 正常完成 |
| LAPI-005A-04 | child on_init 返回 nil, err | 协程 spawn | nil, {code="init_failed"},name 回滚可再次 spawn |
| LAPI-005A-05 | handler 调 shield.exit | send exit_now | service 停止,name 注销 |
| LAPI-005A-06 | shield.self():node() | call self_node | 0(本地) |
| LAPI-005A-07 | handler 调 shield.panic("test panic") | watcher 观察 | on_panic 收到 {type="explicit"},service 退出 |
LAPI-006 Context
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-006-01 | send from A to B | B receives ctx.sender | equals A handle |
| LAPI-006-02 | handler returns | saved callback reads sender | nil or context_expired |
| LAPI-006-03 | timer callback | ctx.sender | nil |
| LAPI-006-04 | trace enabled | call chain | trace id propagated |
| LAPI-006-05 | deadline enabled | call_timeout | remaining deadline visible |
LAPI-007 Timers And Tasks
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-007-01 | timer_once | advance clock | callback once |
| LAPI-007-02 | timer fixed-delay | callback takes time | next run after callback completes |
| LAPI-007-03 | cancel timer | advance clock | callback not called |
| LAPI-007-04 | timer error | callback throws | on_error called,timer stops |
| LAPI-007-05 | sleep | coroutine sleeps | runtime can process other message |
| LAPI-007-06 | fork | task runs | owner service tracked |
| LAPI-007-07 | service exit | owned timers/tasks | canceled |
LAPI-008 Plugin Data API
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-008-01 | database.default -> db.main binding configured | shield.database.mysql("database.default"):query(...) | true, rows or plugin-specific SQL result |
| LAPI-008-02 | binding missing | shield.database.mysql("database.default") | nil, module_unavailable |
| LAPI-008-03 | target instance failed/unavailable | data plugin namespace call | nil, module_unavailable |
| LAPI-008-04 | SQL plugin returns query error | db:query(...) | false, db_query_failed or plugin-mapped database error |
| LAPI-008-05 | cache.session -> cache.redis binding configured | shield.cache.redis("cache.session"):get(key) | true, value or true, nil for miss |
| LAPI-008-06 | cache binding missing | shield.cache.redis("cache.session") | nil, module_unavailable |
| LAPI-008-07 | queue binding configured | shield.queue.redis("queue.events"):subscribe(...); service exit | subscription canceled |
| LAPI-008-08 | SQL transaction supported | db:transaction(fn) | commit forwards payload; rollback returns reason; closed tx rejected |
| LAPI-008-09 | document binding configured | shield.database.mongodb("document.default"):find(...) | true, cursor/docs |
| LAPI-008-10 | leaderboard binding configured | shield.leaderboard.redis("leaderboard.default"):top_n(...) | true, entries |
LAPI-009 Gateway API
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-009-01 | gateway service | typed Bound | on_client_bound(ctx, client) called |
| LAPI-009-02 | ingress request forms | deliver decoded/raw body | 编译绑定 handler(ctx, client, request) called;request 按 descriptor 契约成形 |
| LAPI-009-03 | disconnect | typed Disconnected | on_disconnect(ctx, client, reason) called |
| LAPI-009-04 | kick | typed Unbound | on_client_unbound(ctx, client, reason) called |
| LAPI-009-05 | unknown client | ingress before bind | handler 正常执行,无 session 记录 |
LAPI-010 Legacy API Rejection
| Case | 操作 | 断言 |
|---|---|---|
| LAPI-010-01 | shield.service("x") | nil or legacy_api_removed |
| LAPI-010-02 | shield.plugin.on(...) | nil or legacy_api_removed |
| LAPI-010-03 | shield.db.query(...) / shield.redis.get(...) | unavailable or legacy_api_removed; use plugin namespace |
| LAPI-010-04 | shield.db:query(...) / shield.redis:get(...) | unavailable or legacy_api_removed; use plugin namespace |
| LAPI-010-05 | service only defines on_message(src, type, data) | send method does not dispatch through legacy entrypoint |
| LAPI-010-06 | DI injection API | unavailable |
LAPI-010A Local Event API
未实现(目标契约):
shield.event当前源码尚未实现(全仓无注册点,无对应测试),本组用例为目标契约预留,实现后按本表补齐。
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-010A-01 | 当前 service 注册 listener | shield.event.emit(name, payload) | listener 在同一 VM 内同步收到 payload |
| LAPI-010A-02 | listener unsubscribe | 调用返回的 off() 后 emit | listener 不再触发 |
| LAPI-010A-03 | listener 抛错 | emit | 触发 on_error(err, {type="event"}),其他 listener 继续 |
| LAPI-010A-04 | 另一个 service 注册同名 listener | 从当前 service emit | 另一个 service 不收到,证明不跨 VM |
| LAPI-010A-05 | lifecycle event name | emit application_ready / shutdown | 只作为普通本地事件名处理,不触发 runtime lifecycle |
LAPI-011 Player Lifecycle
适用前提:shield_player 已启用。未启用时整个 LAPI-011 矩阵跳过,且 shield.player.* 调用应返回 module_unavailable。
编号口径:本文矩阵编号与测试源码的
BOOST_AUTO_TEST_CASE编号两套自成体系,不一一对应,以源码为准(tests/lua_api/test_lua_api_player.cpp)。例如源码LAPI_011_18/19/20是shield.player.Base用例(对应本文 -32/-33),并非本文 -18~-20 的 resolve 用例(resolve 错误矩阵由LAPI_011_08_ResolveErrorMatrix覆盖),且源码无LAPI_011_11~14、LAPI_011_21~33同号用例。legacy 矩阵(LAPI-010)同理:源码LAPI_010_04_OldOnMessageEntryIsNotDispatched/LAPI_010_05_OldDiApiUnavailable对应本文 -05/-06(错位一号),tests/lua_api/test_lua_api_config.cpp另复用LAPI_010_01~03前缀(shield.config子系统)。
| Case | 设置 | 操作 | 断言 |
|---|---|---|---|
| LAPI-011-01 | setup 缺 auth 字段 | spawn player service | nil, setup_invalid |
| LAPI-011-02 | setup 缺 login 字段 | spawn player service | nil, setup_invalid |
| LAPI-011-03 | setup 缺 client_message 字段 | spawn player service | nil, setup_invalid |
| LAPI-011-04 | setup 完整且 ready 缺省 | 登录成功 | 默认 ready 标记 player.state="ready",开始分发 client_message |
| LAPI-011-05 | setup 提供 ready 字段 | 登录成功 | 调用业务 ready 后进入 ready;不是 service/application on_ready |
| LAPI-011-06 | ready 前收到客户端业务消息 | deliver payload | 排队或拒绝,但不得调用 client_message |
| LAPI-011-07 | setup 缺 disconnect 字段 | 触发断线 | 进入重连窗口,默认实现被调用 |
| LAPI-011-08 | setup 缺 logout 字段 | 玩家离线 | PlayerManager.unregister 被调用 |
| LAPI-011-09 | setup 缺 save 字段且未配置 persistence | 触发定时保存 | no-op,无数据插件调用 |
| LAPI-011-10 | setup 覆盖 disconnect | 触发断线 | 业务实现被调用,默认实现不执行 |
| LAPI-011-11 | setup 完整 + persistence 启用 | 触发 on_save 默认实现 | adapter 通过配置的插件 binding 持久化白名单字段 |
| LAPI-011-12 | setup 完整 + persistence 未启用 | 触发 on_save 默认实现 | no-op,无数据插件调用 |
| LAPI-011-13 | persistence save 失败(on_save_error="log") | adapter 返回错误 | 错误码 persistence_save_failed,service 继续运行 |
| LAPI-011-14 | persistence save 失败(on_save_error="panic") | adapter 返回错误 | 触发 on_panic |
| LAPI-011-15 | persistence 字段含 function | setup | nil, setup_invalid |
| LAPI-011-16 | PlayerRef LuaPack 编码 | encode | 独立 type tag,字段完整 |
| LAPI-011-17 | shield.player.resolve 本地 ready 玩家 | 解析 ref | 返回 PlayerSession |
| LAPI-011-18 | shield.player.resolve 本地已下线玩家 | 解析 ref | nil, player_not_found |
| LAPI-011-19 | shield.player.resolve 远端 ref(remote resolve 未启用) | 解析 ref | nil, remote_resolve_unimplemented |
| LAPI-011-20 | shield.player.resolve 字段非法 | 解析 ref | nil, invalid_player_ref |
| LAPI-011-21 | cross-service 传 PlayerRef | send payload | receiver 拿到等价 ref |
| LAPI-011-22 | cross-service 传裸连接句柄(非 ClientContext/ClientRef) | send payload | runtime 拒绝并返回错误 |
| LAPI-011-23 | cross-service 传完整 PlayerSession | send payload | runtime 拒绝并返回错误 |
| LAPI-011-24 | auth 返回 anonymous=true 且未开启 anonymous | 认证 | nil, anonymous_disabled |
| LAPI-011-25 | anonymous 已开启且 auth 返回 anonymous=true | 认证 | 状态进入 anonymous,默认不触发 persistence |
| LAPI-011-26 | auth 返回 spectator=true 且未开启 spectator | 认证 | nil, spectator_disabled |
| LAPI-011-27 | spectator 已开启 | 调用 player:set_data 或 player:save | 拒绝并返回 spectator_readonly |
| LAPI-011-28 | multi_device single 且 UID 已在线 | 新连接认证 | false, already_online |
| LAPI-011-29 | multi_device kick_old 且 UID 已在线 | 新连接认证 | 旧会话 logout reason 为 replaced |
| LAPI-011-30 | multi_device multi 且超过 max_devices | 新连接认证 | false, too_many_devices |
| LAPI-011-31 | player_pool 模式 | 同一 uid 多条消息 | 路由到同一 shard 且单玩家 handler 串行 |
| LAPI-011-32 | shield.player.Base.setup(M, opts)(P2 语法糖,OD-014:不引入第二套 lifecycle、不做继承、钩子名无 on_* 前缀) | spawn player service,模块上同名钩子齐备 | 行为等价于 shield.player.setup(M, opts);缺必填钩子 nil, setup_invalid;opts 显式钩子优先;模块未启用 module_unavailable |
| LAPI-011-33 | Base 覆盖可选 hook | 触发 hook | 默认实现不自动执行 |
验收要求
- 当前 CTest 已有
shield_runtime_lua_smoke覆盖 YAML actors 启动、shield.self/now/spawn/send/call/sender/names/exit/on_exit的单节点同步路径;shield_runtime_registry_smoke覆盖query/register/unregister/names的本地 registry 路径; 插件系统相关 smoke 覆盖 manifest scan、instance、binding、register_lua分发和缺失 binding 的module_unavailable返回形态。 它们不是完整 LAPI 矩阵的替代品。 - Lua API 契约变更必须先更新本文。
- 每个新增 API 至少补充一个成功用例和一个失败用例。
- 每个错误码必须在测试中至少出现一次。
examples/hello_world/可以更贴近用户体验,但不能替代上述测试。
剩余延迟用例
以下用例仍依赖后续实现或专用 mock harness,当前尚未完整验证。已完成的历史延迟项保留删除线,作为状态记录。
| Case | 延迟原因 |
|---|---|
call_timeout_atom 驱动,LAPI-005-06 覆盖协程 timeout + 同步调用 | |
resume_caller 在 pending_calls 中找不到 session,静默丢弃。行为正确 | |
CallApiFromLuaWrapsRuntimeResult 测试覆盖嵌套 call | |
| LAPI-006-04 | trace id 传播:消息字段携带通道已实现(send/call 会把 current_trace_id 写入消息),但 runtime 当前不生成 trace id,ctx.trace 恒返回 nil。trace id 生成与完整链路传播属于 Phase 2+ |
ctx.deadline 从 dispatch context 读取,字段随消息传播;但 runtime 当前不为 call 赋值 deadline,ctx.deadline 恒返回 nil。deadline 赋值策略属于 Phase 2+ | |
on_error hook 调用invoke_hook 调用 service table 上的 on_error,OnErrorHookCalledOnHandlerThrow 测试覆盖 | |
shield.sleep coroutine 语义 | |
module_unavailable 需要插件 mock harness 覆盖tests/plugin/test_plugin_lua_facade.cpp 用 fake host resolver(binding_instance_id 恒报 absent)逐插件固定软失败形状——shield.<ns>("ghost.binding") → nil, {code="module_unavailable"}(含 no-arg 调用);namespace 侧 resolver 解析不到 instance 即返回该形状 | |
| LAPI-008-04 | SQL error 路径需要由 SQL 插件测试或统一 mock 插件覆盖 |
| LAPI-008-07 | subscribe then exit:需要 queue plugin mock/集成测试覆盖订阅生命周期 |
| LAPI-009-real-session | 真实 TCP session → ClientContext/ClientRef 的闭环 |
on_exit 中调用 shield.call 返回 api_not_allowed_in_exit_is_in_exit() + Lua wrapper guard,OnExitCallGuard 测试覆盖 |