379 lines
14 KiB
Python
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()
|