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" "\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" "\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()