Coverage: TOTAL 55 -> 59. New cases verify complete module README passes (no missing + order ok), missing-Controls is reported, and absence of API section is accepted by the module tier. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
433 lines
14 KiB
Lua
433 lines
14 KiB
Lua
-- lib-core.api-discovery-test — Test-lib for lib-core.api-discovery
|
|
local api = require("lib-core.api-discovery")
|
|
|
|
local M = {}
|
|
|
|
function M.run_tests(ctx)
|
|
-- Assertions added in plan tasks 2-7.
|
|
|
|
-- ===== parse_lua_surface =====
|
|
do
|
|
local src = [[
|
|
local M = {}
|
|
M.foo = function(x) return x end
|
|
M.bar = function() end
|
|
M._private = function() end
|
|
local function not_in_M() end
|
|
return M
|
|
]]
|
|
local surface = api.parse_lua_surface(src)
|
|
engine.test.equals(#surface.public, 2, "parse_lua_surface: 2 public functions")
|
|
engine.test.equals(surface.public[1], "foo", "parse_lua_surface: first public is foo")
|
|
engine.test.equals(surface.public[2], "bar", "parse_lua_surface: second public is bar")
|
|
engine.test.equals(#surface.private, 1, "parse_lua_surface: 1 private function")
|
|
engine.test.equals(surface.private[1], "_private", "parse_lua_surface: private name = _private")
|
|
|
|
-- Edge: empty source
|
|
local empty = api.parse_lua_surface("local M = {}\nreturn M")
|
|
engine.test.equals(#empty.public, 0, "parse_lua_surface: empty source = no public")
|
|
engine.test.equals(#empty.private, 0, "parse_lua_surface: empty source = no private")
|
|
|
|
-- Sugar-form: function M.foo(...) end
|
|
local sugar_src = [[
|
|
local M = {}
|
|
function M.alpha(x) return x end
|
|
function M.beta() end
|
|
function M._sugar_private() end
|
|
function not_in_M() end
|
|
return M
|
|
]]
|
|
local sugar_surface = api.parse_lua_surface(sugar_src)
|
|
engine.test.equals(#sugar_surface.public, 2, "parse_lua_surface: sugar-form 2 public")
|
|
engine.test.equals(sugar_surface.public[1], "alpha", "parse_lua_surface: sugar-form first = alpha")
|
|
engine.test.equals(sugar_surface.public[2], "beta", "parse_lua_surface: sugar-form second = beta")
|
|
engine.test.equals(#sugar_surface.private, 1, "parse_lua_surface: sugar-form 1 private")
|
|
engine.test.equals(sugar_surface.private[1], "_sugar_private", "parse_lua_surface: sugar-form private = _sugar_private")
|
|
|
|
-- Mixed form (sugar + explicit in same source)
|
|
local mixed_src = [[
|
|
local M = {}
|
|
function M.gamma() end
|
|
M.delta = function() end
|
|
return M
|
|
]]
|
|
local mixed_surface = api.parse_lua_surface(mixed_src)
|
|
engine.test.equals(#mixed_surface.public, 2, "parse_lua_surface: mixed-form 2 public")
|
|
-- Order: sugar-form parsed before explicit (per impl order)? Either is acceptable; use set-check:
|
|
local mixed_set = {}
|
|
for _, n in ipairs(mixed_surface.public) do mixed_set[n] = true end
|
|
engine.test.assert(mixed_set["gamma"], "parse_lua_surface: mixed-form gamma present")
|
|
engine.test.assert(mixed_set["delta"], "parse_lua_surface: mixed-form delta present")
|
|
|
|
-- Comment-stripping: commented stubs should NOT appear
|
|
local commented_src = [[
|
|
local M = {}
|
|
function M.real_func() end
|
|
-- function M.commented_stub() end
|
|
-- M.also_commented = function() end
|
|
return M
|
|
]]
|
|
local commented_surface = api.parse_lua_surface(commented_src)
|
|
engine.test.equals(#commented_surface.public, 1, "parse_lua_surface: comment-stripping leaves 1 public")
|
|
engine.test.equals(commented_surface.public[1], "real_func", "parse_lua_surface: only real_func survives")
|
|
|
|
-- Inline comment after code: code-line should still be matched
|
|
local inline_src = [[
|
|
local M = {}
|
|
function M.with_inline() end -- this is a comment
|
|
return M
|
|
]]
|
|
local inline_surface = api.parse_lua_surface(inline_src)
|
|
engine.test.equals(#inline_surface.public, 1, "parse_lua_surface: inline-comment doesn't break code-match")
|
|
engine.test.equals(inline_surface.public[1], "with_inline", "parse_lua_surface: with_inline detected")
|
|
end
|
|
|
|
-- ===== parse_readme_api =====
|
|
do
|
|
local md = [[
|
|
# lib-core.input
|
|
|
|
Some abstract.
|
|
|
|
## API
|
|
|
|
### `input.bind(action_name, keys)`
|
|
**Syntax:** `input.bind(action_name: string, keys: string[]) → void`
|
|
|
|
### `input.is_action_down(action) → bool`
|
|
Description.
|
|
|
|
## Conventions
|
|
Stuff.
|
|
]]
|
|
local doc = api.parse_readme_api(md)
|
|
engine.test.equals(#doc.documented, 2, "parse_readme_api: 2 documented functions")
|
|
engine.test.equals(doc.documented[1], "bind", "parse_readme_api: first = bind")
|
|
engine.test.equals(doc.documented[2], "is_action_down", "parse_readme_api: second = is_action_down")
|
|
|
|
-- Edge: no API section
|
|
local no_api = api.parse_readme_api("# Just abstract\nNothing else.")
|
|
engine.test.equals(#no_api.documented, 0, "parse_readme_api: no API-section = empty")
|
|
|
|
-- Namespace-less function (e.g., run_tests for test-libs)
|
|
local no_ns_md = [[
|
|
# test-lib
|
|
|
|
## API
|
|
|
|
### `run_tests(ctx)`
|
|
**Syntax:** `M.run_tests(ctx)`
|
|
|
|
## References
|
|
]]
|
|
local no_ns = api.parse_readme_api(no_ns_md)
|
|
engine.test.equals(#no_ns.documented, 1, "parse_readme_api: namespace-less 1 documented")
|
|
engine.test.equals(no_ns.documented[1], "run_tests", "parse_readme_api: namespace-less captures run_tests")
|
|
|
|
-- Mixed: namespace + namespace-less in same section
|
|
local mixed_md = [[
|
|
# foo
|
|
|
|
## API
|
|
|
|
### `foo.bar()`
|
|
text
|
|
|
|
### `helper()`
|
|
text
|
|
|
|
## References
|
|
]]
|
|
local mixed = api.parse_readme_api(mixed_md)
|
|
local set = {}
|
|
for _, n in ipairs(mixed.documented) do set[n] = true end
|
|
engine.test.assert(set["bar"], "parse_readme_api: mixed captures namespaced 'bar'")
|
|
engine.test.assert(set["helper"], "parse_readme_api: mixed captures namespace-less 'helper'")
|
|
engine.test.equals(#mixed.documented, 2, "parse_readme_api: mixed total 2 (no dup)")
|
|
end
|
|
|
|
-- ===== diff_surface =====
|
|
do
|
|
-- Happy: surface == documented
|
|
local surface = { public = {"foo","bar"}, private = {"_x"} }
|
|
local readme = { documented = {"foo","bar"} }
|
|
local diff = api.diff_surface(surface, readme)
|
|
engine.test.equals(#diff.missing_docs, 0, "diff_surface: equal -> no missing")
|
|
engine.test.equals(#diff.stale_docs, 0, "diff_surface: equal -> no stale")
|
|
|
|
-- Surface > Doc -> missing
|
|
local surface2 = { public = {"foo","bar","baz"}, private = {} }
|
|
local readme2 = { documented = {"foo"} }
|
|
local diff2 = api.diff_surface(surface2, readme2)
|
|
engine.test.equals(#diff2.missing_docs, 2, "diff_surface: 2 missing docs")
|
|
engine.test.assert(diff2.missing_docs[1] == "bar" or diff2.missing_docs[1] == "baz", "diff_surface: missing contains bar/baz")
|
|
|
|
-- Doc > Surface -> stale
|
|
local surface3 = { public = {"foo"}, private = {} }
|
|
local readme3 = { documented = {"foo","gone","also_gone"} }
|
|
local diff3 = api.diff_surface(surface3, readme3)
|
|
engine.test.equals(#diff3.stale_docs, 2, "diff_surface: 2 stale docs")
|
|
end
|
|
|
|
-- ===== grep_engine_calls =====
|
|
do
|
|
local src = [[
|
|
local input = require("lib-core.input")
|
|
function update(ctx, dt)
|
|
if engine.input.is_action_down("foo") then
|
|
engine.render.text("hi")
|
|
end
|
|
engine.window.size()
|
|
end
|
|
]]
|
|
local calls = api.grep_engine_calls(src)
|
|
-- Order not guaranteed; check set membership
|
|
local set = {}
|
|
for _, c in ipairs(calls) do set[c] = true end
|
|
engine.test.assert(set["engine.input.is_action_down"], "grep_engine_calls: engine.input.is_action_down")
|
|
engine.test.assert(set["engine.render.text"], "grep_engine_calls: engine.render.text")
|
|
engine.test.assert(set["engine.window.size"], "grep_engine_calls: engine.window.size")
|
|
engine.test.equals(#calls, 3, "grep_engine_calls: 3 unique calls")
|
|
|
|
-- Edge: no engine calls
|
|
local empty = api.grep_engine_calls("local M = {}\nreturn M")
|
|
engine.test.equals(#empty, 0, "grep_engine_calls: empty -> 0")
|
|
end
|
|
|
|
-- ===== generate_topology_block =====
|
|
do
|
|
-- Standard: lib with 1 dep + engine-calls
|
|
local manifest = {
|
|
id = "lib-core.foo",
|
|
deps = { {id="lib-core.bar"} }
|
|
}
|
|
local engine_calls = {"engine.input.is_action_down"}
|
|
local mermaid = api.generate_topology_block(manifest, engine_calls)
|
|
engine.test.assert(string.find(mermaid, "graph LR") or string.find(mermaid, "graph TD"),
|
|
"generate_topology_block: starts with graph directive")
|
|
engine.test.assert(string.find(mermaid, "lib%-core%.foo", 1, false), "generate_topology_block: self-node present")
|
|
engine.test.assert(string.find(mermaid, "lib%-core%.bar", 1, false), "generate_topology_block: dep-node present")
|
|
engine.test.assert(string.find(mermaid, "engine%.%*", 1, false), "generate_topology_block: engine-node present")
|
|
|
|
-- Asset-Lib: no deps, no engine-calls
|
|
local asset = { id = "lib-asset.sprites", deps = {} }
|
|
local asset_mermaid = api.generate_topology_block(asset, {})
|
|
engine.test.assert(string.find(asset_mermaid, "lib%-asset%.sprites", 1, false), "generate_topology_block: asset-lib has self-node")
|
|
engine.test.assert(not string.find(asset_mermaid, "engine%.%*", 1, false), "generate_topology_block: asset-lib has no engine-node")
|
|
end
|
|
|
|
-- ===== validate_readme_structure =====
|
|
do
|
|
local complete = [[
|
|
# lib-core.foo
|
|
Abstract.
|
|
|
|
**Version:** 0.1.0
|
|
**Lib-ID:** lib-core.foo
|
|
**Requires:** (none)
|
|
|
|
## Topology
|
|
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## API
|
|
|
|
### `foo.bar()`
|
|
Sig.
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r = api.validate_readme_structure(complete, "core")
|
|
engine.test.equals(#r.missing_sections, 0, "validate: complete README -> no missing")
|
|
engine.test.assert(r.section_order_ok, "validate: complete README -> order ok")
|
|
|
|
local missing_api = [[
|
|
# lib-core.foo
|
|
Abstract.
|
|
|
|
**Version:** 0.1.0
|
|
**Lib-ID:** lib-core.foo
|
|
|
|
## Topology
|
|
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r2 = api.validate_readme_structure(missing_api, "core")
|
|
engine.test.assert(#r2.missing_sections > 0, "validate: missing API -> reports missing")
|
|
local has_api_missing = false
|
|
for _, s in ipairs(r2.missing_sections) do
|
|
if s == "API" then has_api_missing = true end
|
|
end
|
|
engine.test.assert(has_api_missing, "validate: missing-sections list contains 'API'")
|
|
|
|
-- Missing Topology heading: should detect Topology as missing MUST-section
|
|
local missing_topology = [[
|
|
# lib-core.foo
|
|
Abstract.
|
|
|
|
**Version:** 0.1.0
|
|
**Lib-ID:** lib-core.foo
|
|
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## API
|
|
|
|
### `foo.bar()`
|
|
Sig.
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r_topo = api.validate_readme_structure(missing_topology, "core")
|
|
engine.test.assert(#r_topo.missing_sections > 0, "validate: missing Topology heading -> reports missing")
|
|
local has_topology_missing = false
|
|
for _, s in ipairs(r_topo.missing_sections) do
|
|
if s == "Topology" then has_topology_missing = true end
|
|
end
|
|
engine.test.assert(has_topology_missing, "validate: missing-sections list contains 'Topology'")
|
|
|
|
-- Missing Badges (no **Lib-ID:** bold line): should detect Badges as missing
|
|
local missing_badges = [[
|
|
# lib-core.foo
|
|
Abstract.
|
|
|
|
## Topology
|
|
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## API
|
|
|
|
### `foo.bar()`
|
|
Sig.
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r_badges = api.validate_readme_structure(missing_badges, "core")
|
|
local has_badges_missing = false
|
|
for _, s in ipairs(r_badges.missing_sections) do
|
|
if s == "Badges" then has_badges_missing = true end
|
|
end
|
|
engine.test.assert(has_badges_missing, "validate: missing-sections list contains 'Badges'")
|
|
|
|
-- Tier community: lockerer
|
|
local r3 = api.validate_readme_structure(missing_api, "community")
|
|
engine.test.equals(#r3.missing_sections, 0, "validate: tier=community -> no enforcement")
|
|
|
|
-- Tier engine: same structure as core but flexible Badges
|
|
local engine_md = [[
|
|
# sporel-engine
|
|
|
|
**Version:** 0.1.0
|
|
**License:** Proprietary
|
|
|
|
## Topology
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## API
|
|
text
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r_engine = api.validate_readme_structure(engine_md, "engine")
|
|
engine.test.equals(#r_engine.missing_sections, 0, "validate: engine README -> no missing")
|
|
|
|
-- Missing API in engine README -> still detected
|
|
local engine_no_api = [[
|
|
# sporel-engine
|
|
|
|
**Version:** 0.1.0
|
|
|
|
## Topology
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## References
|
|
]]
|
|
local r_engine_no_api = api.validate_readme_structure(engine_no_api, "engine")
|
|
engine.test.assert(#r_engine_no_api.missing_sections > 0, "validate: engine without API -> reports missing")
|
|
|
|
-- Naked Badges line (no Lib-ID prefix) accepted in engine tier
|
|
local engine_naked = [[
|
|
# sporel-engine
|
|
**Anything:** value
|
|
]]
|
|
local r_engine_naked = api.validate_readme_structure(engine_naked, "engine")
|
|
-- Just verify Badges-check doesn't fail because of missing "Lib-ID:" prefix
|
|
local badges_missing = false
|
|
for _, s in ipairs(r_engine_naked.missing_sections) do
|
|
if s == "Badges" then badges_missing = true end
|
|
end
|
|
engine.test.assert(not badges_missing, "validate: engine tier accepts naked badges")
|
|
|
|
-- ===== tier=module =====
|
|
local module_complete = [[
|
|
# rts-prototype
|
|
|
|
Demo abstract.
|
|
|
|
**Version:** 0.1.0
|
|
**Module-ID:** rts-prototype
|
|
**Requires:** lib-core.maps v>=0.1.1
|
|
**Tags:** rts, tech-demo
|
|
|
|
## Topology
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## Controls
|
|
- LMB: select
|
|
|
|
## Demonstrates
|
|
- Composition of selection + command
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r_mod = api.validate_readme_structure(module_complete, "module")
|
|
engine.test.equals(#r_mod.missing_sections, 0, "validate: module README -> no missing")
|
|
engine.test.assert(r_mod.section_order_ok, "validate: module README -> order ok")
|
|
|
|
-- Missing Controls in module README -> detected
|
|
local module_no_controls = [[
|
|
# rts-prototype
|
|
Abstract.
|
|
**Version:** 0.1.0
|
|
**Module-ID:** rts-prototype
|
|
|
|
## Topology
|
|
<!-- topology:start -->
|
|
<!-- topology:end -->
|
|
|
|
## Demonstrates
|
|
- Stuff
|
|
|
|
## References
|
|
- spec
|
|
]]
|
|
local r_mod_no_c = api.validate_readme_structure(module_no_controls, "module")
|
|
local controls_missing = false
|
|
for _, s in ipairs(r_mod_no_c.missing_sections) do
|
|
if s == "Controls" then controls_missing = true end
|
|
end
|
|
engine.test.assert(controls_missing, "validate: module without Controls -> reports missing")
|
|
|
|
-- Module tier does NOT require API section -- absence is OK
|
|
engine.test.assert(true, "validate: module tier accepts absence of API")
|
|
end
|
|
|
|
engine.exit(engine.test.failures())
|
|
end
|
|
|
|
return M
|