Files
playbook/tools/tsl-codegen/tests/test_build_index.py
T
csh 9edc8fc868 feat(tsl-codegen): support unified API declarations
Add TSF conversion and shared Markdown recognition for mixed
function, class, and unit declarations.

Extend generation, linting, indexing, examples, and tests around
the unified declaration model.
2026-07-29 15:45:50 +08:00

379 lines
14 KiB
Python

import importlib.util
import io
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parents[1]
/ "scripts"
/ "build_index.py"
)
def load_script():
spec = importlib.util.spec_from_file_location(
"tsl_codegen_function_index", SCRIPT_PATH
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class FunctionIndexTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.skill_dir = Path(self.temp_dir.name) / "tsl-api-reference"
self.codegen_root = self.skill_dir / "references" / "codegen"
self.data_dir = self.skill_dir / "data"
leaf = self.codegen_root / "builtin" / "base" / "array.md"
leaf.parent.mkdir(parents=True)
leaf.write_text(
"# Builtin - 基础 / 数组\n\n"
"## `demo()`\n\n"
"声明:function\n\n"
"返回示例值。\n\n"
"<!-- tags: 数组 列表 -->\n\n"
"返回:integer\n",
encoding="utf-8",
)
self.module = load_script()
def tearDown(self):
self.temp_dir.cleanup()
def run_main(self, *args):
stdout = io.StringIO()
stderr = io.StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr):
result = self.module.main(["--skill-dir", str(self.skill_dir), *args])
return result, stdout.getvalue(), stderr.getvalue()
def write_page(self, relative_path, text):
page = self.codegen_root / relative_path
page.parent.mkdir(parents=True, exist_ok=True)
page.write_text(text, encoding="utf-8")
return page
def test_rebuild_writes_only_tsv(self):
result, _, _ = self.run_main()
self.assertEqual(0, result)
self.assertTrue((self.data_dir / "function_index.tsv").is_file())
self.assertEqual([], list(self.codegen_root.rglob("index.md")))
def test_tags_and_summary_are_stored_separately(self):
rows = self.module.build_rows(self.codegen_root)
row = dict(zip(self.module.HEADER, rows[0]))
self.assertEqual("数组 列表", row["tags"])
self.assertEqual("返回示例值。", row["summary"])
def test_declaration_does_not_become_a_missing_description_summary(self):
self.write_page(
"project/missing_description.md",
"# Missing\n\n"
"## `Missing()`\n\n"
"声明:function\n\n"
"返回:nil\n",
)
rows = {
row[0]: dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual("", rows["Missing"]["summary"])
def test_legacy_function_anchor_and_first_eight_columns_are_unchanged(self):
rows = self.module.build_rows(self.codegen_root)
row = dict(zip(self.module.HEADER, rows[0]))
self.assertEqual(
[
"name",
"scope",
"module",
"signature",
"page",
"anchor",
"tags",
"summary",
"kind",
"binding",
"visibility",
"owner",
"qualified_name",
],
self.module.HEADER,
)
self.assertEqual(
[
"demo",
"builtin",
"base",
"demo()",
"builtin/base/array.md",
"demo",
"数组 列表",
"返回示例值。",
],
rows[0][:8],
)
self.assertEqual("function", row["kind"])
self.assertEqual("", row["binding"])
self.assertEqual("", row["visibility"])
self.assertEqual("", row["owner"])
self.assertEqual("demo", row["qualified_name"])
def test_mixed_page_resets_owner_for_every_h2(self):
self.write_page(
"project/mixed/api.md",
"# Mixed\n\n"
"## `Widget`\n\n声明:class\n\n组件。\n\n"
"### `Open()`\n\n声明:function\n\n打开。\n\n可见性:public\n\n"
"## `Parse()`\n\n声明:function\n\n解析。\n\n返回:Widget\n\n"
"## `Runtime`\n\n声明:unit\n\n接口。\n\n"
"### `Document`\n\n声明:class\n\n文档。\n\n"
"#### `Save()`\n\n声明:function\n\n保存。\n\n可见性:public\n",
)
rows = {
(row[0], row[3]): dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual("", rows[("Widget", "Widget")]["owner"])
self.assertEqual("Widget", rows[("Open", "Open()")]["owner"])
self.assertEqual("", rows[("Parse", "Parse()")]["owner"])
self.assertEqual("", rows[("Runtime", "Runtime")]["owner"])
self.assertEqual("Runtime", rows[("Document", "Document")]["owner"])
self.assertEqual(
"Runtime.Document", rows[("Save", "Save()")]["owner"]
)
def test_class_page_maps_members_to_unified_index_columns(self):
self.write_page(
"project/widgets/widget.md",
"# Project / Widgets\n\n"
"## `Widget`\n\n"
"声明:class\n\n"
"组件。\n\n"
"<!-- tags: 组件 -->\n\n"
"### `Open(path)`\n\n"
"声明:function\n\n"
"打开组件。\n\n"
"可见性:`public`\n\n"
"### `Create()`\n\n"
"声明:class function\n\n"
"创建组件。\n\n"
"可见性:`protected`\n\n"
"### `Count`\n\n"
"声明:static field\n\n"
"组件数量。\n\n"
"可见性:`protected`\n\n"
"类型:integer\n\n"
"### `Items(index)`\n\n"
"声明:property\n\n"
"按索引读取组件。\n\n"
"可见性:`public`\n\n"
"类型:Widget\n\n"
"访问:read\n\n"
"### `DefaultName`\n\n"
"声明:const\n\n"
"默认名称。\n\n"
"可见性:`public`\n\n"
"值:`'widget'`\n\n"
"### `Maximum`\n\n"
"声明:static const\n\n"
"最大数量。\n\n"
"可见性:`protected`\n\n"
"值:`100`\n",
)
rows = {
row[self.module.HEADER.index("qualified_name")]: dict(
zip(self.module.HEADER, row)
)
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual(
{
"name": "Widget",
"scope": "project",
"module": "widgets",
"signature": "Widget",
"page": "project/widgets/widget.md",
"anchor": "widget",
"tags": "组件",
"summary": "组件。",
"kind": "class",
"binding": "",
"visibility": "",
"owner": "",
"qualified_name": "Widget",
},
rows["Widget"],
)
self.assertEqual("method", rows["Widget.Open"]["kind"])
self.assertEqual("instance", rows["Widget.Open"]["binding"])
self.assertEqual("public", rows["Widget.Open"]["visibility"])
self.assertEqual("Widget", rows["Widget.Open"]["owner"])
self.assertEqual("openpath", rows["Widget.Open"]["anchor"])
self.assertEqual("class", rows["Widget.Create"]["binding"])
self.assertEqual("create", rows["Widget.Create"]["anchor"])
self.assertEqual("field", rows["Widget.Count"]["kind"])
self.assertEqual("static", rows["Widget.Count"]["binding"])
self.assertEqual("count", rows["Widget.Count"]["anchor"])
self.assertEqual("property", rows["Widget.Items"]["kind"])
self.assertEqual("instance", rows["Widget.Items"]["binding"])
self.assertEqual("Items(index)", rows["Widget.Items"]["signature"])
self.assertEqual("constant", rows["Widget.DefaultName"]["kind"])
self.assertEqual("instance", rows["Widget.DefaultName"]["binding"])
self.assertEqual("constant", rows["Widget.Maximum"]["kind"])
self.assertEqual("static", rows["Widget.Maximum"]["binding"])
self.assertEqual("maximum", rows["Widget.Maximum"]["anchor"])
def test_unit_page_derives_nested_owner_and_public_interface_visibility(self):
self.write_page(
"dotnet/runtime/demo_unit.md",
"# Dotnet / Runtime\n\n"
"## `DemoUnit`\n\n"
"声明:unit\n\n"
"接口。\n\n"
"### `Open()`\n\n"
"声明:function\n\n"
"打开。\n\n"
"返回:Document\n\n"
"### `Current`\n\n"
"声明:var\n\n"
"当前文档。\n\n"
"类型:Document\n\n"
"### `DefaultName`\n\n"
"声明:const\n\n"
"默认名称。\n\n"
"值:`'demo'`\n\n"
"### `Document`\n\n"
"声明:class\n\n"
"文档对象。\n\n"
"#### `Save()`\n\n"
"声明:function\n\n"
"保存。\n\n"
"可见性:`protected`\n",
)
rows = {
row[self.module.HEADER.index("qualified_name")]: dict(
zip(self.module.HEADER, row)
)
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual("unit", rows["DemoUnit"]["kind"])
self.assertEqual("", rows["DemoUnit"]["owner"])
self.assertEqual("unit", rows["DemoUnit.Open"]["binding"])
self.assertEqual("public", rows["DemoUnit.Open"]["visibility"])
self.assertEqual("DemoUnit", rows["DemoUnit.Open"]["owner"])
self.assertEqual("variable", rows["DemoUnit.Current"]["kind"])
self.assertEqual("constant", rows["DemoUnit.DefaultName"]["kind"])
self.assertEqual("unit", rows["DemoUnit.DefaultName"]["binding"])
self.assertEqual("public", rows["DemoUnit.DefaultName"]["visibility"])
self.assertEqual("unit", rows["DemoUnit.Document"]["binding"])
self.assertEqual("public", rows["DemoUnit.Document"]["visibility"])
self.assertEqual("DemoUnit.Document", rows["DemoUnit.Document.Save"]["owner"])
self.assertEqual("method", rows["DemoUnit.Document.Save"]["kind"])
self.assertEqual("instance", rows["DemoUnit.Document.Save"]["binding"])
self.assertEqual("protected", rows["DemoUnit.Document.Save"]["visibility"])
def test_class_overloads_share_qualified_name_but_have_distinct_anchors(self):
self.write_page(
"project/widgets/overloads.md",
"# Project / Widgets\n\n"
"## `Overloads`\n\n"
"声明:class\n\n"
"重载示例。\n\n"
"### `Open(path)`\n\n"
"声明:function\n\n"
"按路径打开。\n\n"
"可见性:`public`\n\n"
"### `Open(mode, flags)`\n\n"
"声明:function\n\n"
"按模式打开。\n\n"
"可见性:`public`\n",
)
rows = [
dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
if row[0] == "Open"
]
self.assertEqual(2, len(rows))
self.assertEqual({"Overloads.Open"}, {row["qualified_name"] for row in rows})
self.assertEqual(
{"openpath", "openmodeflags"},
{row["anchor"] for row in rows},
)
self.assertEqual(
2,
len({f"{row['page']}#{row['anchor']}" for row in rows}),
)
def test_top_level_overloads_and_cross_kind_names_get_unique_anchors(self):
page = "project/mixed/overloads.md"
self.write_page(
page,
"# Project / Mixed\n\n"
"## `Open(path)`\n\n声明:function\n\n按路径打开。\n\n"
"返回:nil\n\n"
"## `Open(mode)`\n\n声明:function\n\n按模式打开。\n\n"
"返回:nil\n\n"
"## `Open`\n\n声明:class\n\n打开器。\n\n"
"## `Open`\n\n声明:unit\n\n打开接口。\n",
)
rows = [
dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
if row[4] == page
]
by_identity = {
(row["kind"], row["signature"]): row for row in rows
}
self.assertEqual(4, len(rows))
self.assertEqual({""}, {row["owner"] for row in rows})
self.assertEqual({"Open"}, {row["qualified_name"] for row in rows})
self.assertEqual(
"open", by_identity[("function", "Open(path)")]["anchor"]
)
self.assertEqual(
"open-1", by_identity[("function", "Open(mode)")]["anchor"]
)
self.assertEqual("open-2", by_identity[("class", "Open")]["anchor"])
self.assertEqual("open-3", by_identity[("unit", "Open")]["anchor"])
self.assertEqual(
4, len({f"{row['page']}#{row['anchor']}" for row in rows})
)
def test_check_does_not_require_index_pages(self):
self.data_dir.mkdir(parents=True)
rows = self.module.build_rows(self.codegen_root)
(self.data_dir / "function_index.tsv").write_text(
self.module.render_tsv(rows),
encoding="utf-8",
newline="\n",
)
result, stdout, _ = self.run_main("--check")
self.assertEqual(0, result)
self.assertIn("matches md tree", stdout)
if __name__ == "__main__":
unittest.main()