@@ -33,8 +33,9 @@ class FunctionIndexTest(unittest.TestCase):
|
||||
leaf.write_text(
|
||||
"# Builtin - 基础 / 数组\n\n"
|
||||
"## `demo()`\n\n"
|
||||
"<!-- tags: 数组 列表 -->\n\n"
|
||||
"声明:function\n\n"
|
||||
"返回示例值。\n\n"
|
||||
"<!-- tags: 数组 列表 -->\n\n"
|
||||
"返回:integer\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -50,6 +51,12 @@ class FunctionIndexTest(unittest.TestCase):
|
||||
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()
|
||||
|
||||
@@ -64,6 +71,294 @@ class FunctionIndexTest(unittest.TestCase):
|
||||
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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -6,7 +7,22 @@ from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).parents[1] / "scripts" / "lint.py"
|
||||
VALID_PAGE = "# 项目 / 示例\n\n## `demo()`\n\n示例函数\n\n返回:nil\n"
|
||||
API_MARKDOWN = Path(__file__).parents[1] / "scripts" / "api_markdown.py"
|
||||
VALID_PAGE = (
|
||||
"# 项目 / 示例\n\n"
|
||||
"## `demo()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"示例函数\n\n"
|
||||
"返回:nil\n"
|
||||
)
|
||||
|
||||
|
||||
def load_api_markdown():
|
||||
spec = importlib.util.spec_from_file_location("tsl_api_markdown", API_MARKDOWN)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class DocLintCliTest(unittest.TestCase):
|
||||
@@ -38,6 +54,781 @@ class DocLintCliTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("1 files", result.stderr)
|
||||
|
||||
def test_accepts_mixed_typed_h2_declarations(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 混合\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"
|
||||
"### 示例\n\n"
|
||||
"```tsl\nreturn Parse();\n```\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",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page, "--strict")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_declaration_must_use_exact_canonical_line(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 函数\n\n"
|
||||
"## `Open()`\n\n"
|
||||
"声明:Function\n\n"
|
||||
"打开。\n\n"
|
||||
"返回:nil\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[heading]", result.stdout)
|
||||
self.assertIn("声明:function", result.stdout)
|
||||
|
||||
def test_business_type_metadata_is_not_a_declaration_marker(self):
|
||||
module = load_api_markdown()
|
||||
lines = [
|
||||
"## `Account`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"远程服务器账户。",
|
||||
"",
|
||||
"类型:读写",
|
||||
"",
|
||||
"返回:nil",
|
||||
]
|
||||
|
||||
entries = list(module.iter_api_entries(lines))
|
||||
|
||||
self.assertEqual(1, len(entries))
|
||||
self.assertTrue(entries[0].heading.valid)
|
||||
self.assertEqual("function", entries[0].heading.root_kind)
|
||||
|
||||
self.page.write_text(
|
||||
"# 示例 / 遗留属性\n\n" + "\n".join(lines) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = self.run_cli("--file", self.page, "--strict")
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_declaration_must_be_first_non_empty_line_after_heading(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 函数\n\n"
|
||||
"## `Open()`\n\n"
|
||||
"打开。\n\n"
|
||||
"声明:function\n\n"
|
||||
"返回:nil\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[heading]", result.stdout)
|
||||
self.assertIn("第一条非空正文", result.stdout)
|
||||
|
||||
def test_tags_must_follow_description(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 函数\n\n"
|
||||
"## `Open()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"<!-- tags: 示例 -->\n\n"
|
||||
"打开。\n\n"
|
||||
"返回:nil\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[description]", result.stdout)
|
||||
|
||||
def test_property_type_is_optional_but_access_is_required(self):
|
||||
page_without_type = (
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Title`\n\n"
|
||||
"声明:property\n\n"
|
||||
"组件标题。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"访问:read\n"
|
||||
)
|
||||
self.page.write_text(page_without_type, encoding="utf-8")
|
||||
|
||||
valid = self.run_cli("--file", self.page, "--strict")
|
||||
|
||||
self.assertEqual(0, valid.returncode, valid.stdout + valid.stderr)
|
||||
|
||||
self.page.write_text(
|
||||
page_without_type.replace("\n访问:read\n", "\n"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
invalid = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, invalid.returncode)
|
||||
self.assertIn("[access]", invalid.stdout)
|
||||
|
||||
def test_accepts_class_page_with_optional_method_return(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Close()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"关闭组件。\n\n"
|
||||
"可见性:`public`\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page, "--strict")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_class_member_requires_visibility(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Count`\n\n"
|
||||
"声明:field\n\n"
|
||||
"组件数量。\n\n"
|
||||
"类型:integer\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[visibility]", result.stdout)
|
||||
|
||||
def test_parameterized_property_requires_the_fixed_parameter_table(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Items(index)`\n\n"
|
||||
"声明:property\n\n"
|
||||
"按索引读取组件。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"类型:Widget\n\n"
|
||||
"访问:read\n\n"
|
||||
"| 名称 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `index` | integer | 索引 |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[param-header]", result.stdout)
|
||||
|
||||
def test_nullary_property_rejects_a_parameter_table(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Title`\n\n"
|
||||
"声明:property\n\n"
|
||||
"组件标题。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"类型:string\n\n"
|
||||
"访问:read\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `unused` | integer | 不应存在 |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[param-table]", result.stdout)
|
||||
|
||||
def test_class_method_allows_h4_value_and_example_subheadings(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Open(mode)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开组件。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `mode` | integer | 打开模式 |\n\n"
|
||||
"#### `mode` 取值\n\n"
|
||||
"- `0` — 默认模式\n\n"
|
||||
"#### 示例\n\n"
|
||||
"```tsl\nreturn Open(0);\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page, "--strict")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_property_rejects_example_subheading(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Title`\n\n"
|
||||
"声明:property\n\n"
|
||||
"组件标题。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"类型:string\n\n"
|
||||
"访问:read\n\n"
|
||||
"#### 示例\n\n"
|
||||
"```tsl\nreturn Widget.Title;\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[subheading]", result.stdout)
|
||||
self.assertIn("property", result.stdout)
|
||||
|
||||
def test_accepts_unit_page_with_nested_class_member(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"提供接口。\n\n"
|
||||
"### `DefaultSize`\n\n"
|
||||
"声明:const\n\n"
|
||||
"默认大小。\n\n"
|
||||
"值:`100`\n\n"
|
||||
"### `Open()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开。\n\n"
|
||||
"返回:Document\n\n"
|
||||
"### `Document`\n\n"
|
||||
"声明:class\n\n"
|
||||
"文档对象。\n\n"
|
||||
"#### `Save()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"保存。\n\n"
|
||||
"可见性:`protected`\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page, "--strict")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_unit_class_method_allows_h5_value_and_example_subheadings(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"提供接口。\n\n"
|
||||
"### `Document`\n\n"
|
||||
"声明:class\n\n"
|
||||
"文档对象。\n\n"
|
||||
"#### `Save(mode)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"保存。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `mode` | integer | 保存模式 |\n\n"
|
||||
"##### `mode` 取值\n\n"
|
||||
"- `0` — 默认模式\n\n"
|
||||
"##### 示例\n\n"
|
||||
"```tsl\nreturn Save(0);\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page, "--strict")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_unit_function_requires_return(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"提供接口。\n\n"
|
||||
"### `Open()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[return]", result.stdout)
|
||||
|
||||
def test_unit_function_rejects_an_empty_return_type(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"提供接口。\n\n"
|
||||
"### `Open()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开。\n\n"
|
||||
"返回:\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[return]", result.stdout)
|
||||
|
||||
def test_empty_type_and_value_lines_are_rejected(self):
|
||||
cases = {
|
||||
"property": (
|
||||
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
||||
"### `Title`\n\n声明:property\n\n标题。\n\n可见性:`public`\n\n"
|
||||
"类型:\n\n访问:read\n",
|
||||
"[type]",
|
||||
),
|
||||
"field": (
|
||||
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
||||
"### `Count`\n\n声明:field\n\n数量。\n\n可见性:`public`\n\n类型:\n",
|
||||
"[type]",
|
||||
),
|
||||
"variable": (
|
||||
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
||||
"### `Current`\n\n声明:var\n\n当前值。\n\n类型:\n",
|
||||
"[type]",
|
||||
),
|
||||
"constant": (
|
||||
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
||||
"### `Default`\n\n声明:const\n\n默认值。\n\n值:\n",
|
||||
"[value]",
|
||||
),
|
||||
}
|
||||
for name, (text, expected) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
self.page.write_text(text, encoding="utf-8")
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(expected, result.stdout)
|
||||
|
||||
def test_class_parent_metadata_does_not_count_as_description(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"父类:`BaseWidget`\n\n"
|
||||
"",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[description]", result.stdout)
|
||||
|
||||
def test_class_parameter_rows_must_match_signature(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Open(mode)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开组件。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `other` | integer | 错误参数 |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[param-names]", result.stdout)
|
||||
|
||||
def test_class_parameter_rows_require_type_and_description(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Open(mode)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开组件。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `mode` | | |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[param-row]", result.stdout)
|
||||
|
||||
def test_unit_class_member_requires_visibility(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"提供接口。\n\n"
|
||||
"### `Document`\n\n"
|
||||
"声明:class\n\n"
|
||||
"文档对象。\n\n"
|
||||
"#### `Name`\n\n"
|
||||
"声明:field\n\n"
|
||||
"文档名称。\n\n"
|
||||
"类型:string\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[visibility]", result.stdout)
|
||||
|
||||
def test_class_and_unit_member_data_lines_are_required(self):
|
||||
cases = {
|
||||
"property": (
|
||||
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
||||
"### `Title`\n\n声明:property\n\n标题。\n\n可见性:`public`\n",
|
||||
"[access]",
|
||||
),
|
||||
"field": (
|
||||
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
||||
"### `Count`\n\n声明:field\n\n数量。\n\n可见性:`public`\n",
|
||||
"[type]",
|
||||
),
|
||||
"variable": (
|
||||
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
||||
"### `Current`\n\n声明:var\n\n当前值。\n",
|
||||
"[type]",
|
||||
),
|
||||
"constant": (
|
||||
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
||||
"### `Default`\n\n声明:const\n\n默认值。\n",
|
||||
"[value]",
|
||||
),
|
||||
}
|
||||
for name, (text, expected) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
self.page.write_text(text, encoding="utf-8")
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(expected, result.stdout)
|
||||
|
||||
def test_class_legacy_typed_api_heading_is_rejected(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### function `Open()`\n\n"
|
||||
"打开组件。\n\n"
|
||||
"可见性:`public`\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[heading]", result.stdout)
|
||||
self.assertIn("标题只写名称或调用签名", result.stdout)
|
||||
|
||||
def test_unit_legacy_typed_api_heading_is_rejected(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"提供接口。\n\n"
|
||||
"### function `Open()`\n\n"
|
||||
"打开。\n\n"
|
||||
"返回:Document\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[heading]", result.stdout)
|
||||
self.assertIn("标题只写名称或调用签名", result.stdout)
|
||||
|
||||
def test_class_member_at_h4_is_rejected(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"#### `Open()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开组件。\n\n"
|
||||
"可见性:`public`\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[heading]", result.stdout)
|
||||
self.assertIn("H3", result.stdout)
|
||||
|
||||
def test_unit_class_member_at_h5_is_rejected(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / Unit\n\n"
|
||||
"## `DemoUnit`\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",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("[heading]", result.stdout)
|
||||
self.assertIn("H4", result.stdout)
|
||||
|
||||
def test_rejects_static_function_and_private_api(self):
|
||||
self.page.write_text(
|
||||
"# 示例 / 类\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"表示组件。\n\n"
|
||||
"### `Create()`\n\n"
|
||||
"声明:static function\n\n"
|
||||
"创建。\n\n"
|
||||
"可见性:`private`\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli("--file", self.page)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("static function", result.stdout)
|
||||
self.assertIn("private", result.stdout)
|
||||
|
||||
def test_mixed_h2_declarations_reset_nested_context(self):
|
||||
module = load_api_markdown()
|
||||
lines = [
|
||||
"# Demo",
|
||||
"",
|
||||
"## `Widget`",
|
||||
"",
|
||||
"声明:class",
|
||||
"",
|
||||
"组件。",
|
||||
"",
|
||||
"### `Open()`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"打开。",
|
||||
"",
|
||||
"可见性:public",
|
||||
"",
|
||||
"## `Parse(text)`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"解析。",
|
||||
"",
|
||||
"返回:Widget",
|
||||
"",
|
||||
"## `DemoUnit`",
|
||||
"",
|
||||
"声明:unit",
|
||||
"",
|
||||
"接口。",
|
||||
"",
|
||||
"### `Document`",
|
||||
"",
|
||||
"声明:class",
|
||||
"",
|
||||
"文档。",
|
||||
"",
|
||||
"#### `Save()`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"保存。",
|
||||
"",
|
||||
"可见性:public",
|
||||
]
|
||||
|
||||
headings = [entry.heading for entry in module.iter_api_entries(lines)]
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
(2, "class", "class", "Widget"),
|
||||
(3, "method", "class", "Open"),
|
||||
(2, "function", "function", "Parse"),
|
||||
(2, "unit", "unit", "DemoUnit"),
|
||||
(3, "class", "unit", "Document"),
|
||||
(4, "method", "unit", "Save"),
|
||||
],
|
||||
[
|
||||
(item.level, item.kind, item.root_kind, item.name)
|
||||
for item in headings
|
||||
],
|
||||
)
|
||||
|
||||
def test_missing_duplicate_and_unknown_declarations_are_errors(self):
|
||||
module = load_api_markdown()
|
||||
cases = {
|
||||
"missing": (
|
||||
["## `Open()`", "", "打开。", "", "返回:nil"],
|
||||
"第一条非空正文",
|
||||
),
|
||||
"duplicate": (
|
||||
[
|
||||
"## `Widget`",
|
||||
"",
|
||||
"声明:class",
|
||||
"",
|
||||
"组件。",
|
||||
"",
|
||||
"声明:unit",
|
||||
],
|
||||
"必须且只能包含一行",
|
||||
),
|
||||
"unknown": (
|
||||
["## `Runtime`", "", "声明:module", "", "接口。"],
|
||||
"必须是 function、class 或 unit",
|
||||
),
|
||||
}
|
||||
for name, (lines, expected) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
headings = [
|
||||
entry.heading for entry in module.iter_api_entries(lines)
|
||||
]
|
||||
|
||||
self.assertEqual(1, len(headings))
|
||||
self.assertFalse(headings[0].valid)
|
||||
self.assertIn(expected, headings[0].error)
|
||||
|
||||
def test_plain_h2_ends_api_body_and_resets_nested_context(self):
|
||||
module = load_api_markdown()
|
||||
lines = [
|
||||
"# Demo",
|
||||
"",
|
||||
"## `Parse()`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"解析。",
|
||||
"",
|
||||
"返回:nil",
|
||||
"",
|
||||
"## 其他说明",
|
||||
"",
|
||||
"### function `NotAMember()`",
|
||||
]
|
||||
|
||||
entries = list(module.iter_api_entries(lines))
|
||||
|
||||
self.assertEqual(["Parse"], [entry.heading.name for entry in entries])
|
||||
self.assertEqual(10, entries[0].end)
|
||||
|
||||
def test_parameter_value_and_example_headings_are_not_api_entries(self):
|
||||
module = load_api_markdown()
|
||||
lines = [
|
||||
"# Demo",
|
||||
"",
|
||||
"## `Widget`",
|
||||
"",
|
||||
"声明:class",
|
||||
"",
|
||||
"表示组件。",
|
||||
"",
|
||||
"### `Open(mode)`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"打开。",
|
||||
"",
|
||||
"可见性:`public`",
|
||||
"",
|
||||
"#### `mode` 取值",
|
||||
"",
|
||||
"#### 示例",
|
||||
"",
|
||||
"## `Parse(mode)`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"解析。",
|
||||
"",
|
||||
"| 参数 | 类型 | 说明 |",
|
||||
"| --- | --- | --- |",
|
||||
"| `mode` | integer | 模式 |",
|
||||
"",
|
||||
"**mode 取值**",
|
||||
"",
|
||||
"### 示例",
|
||||
"",
|
||||
"## `DemoUnit`",
|
||||
"",
|
||||
"声明:unit",
|
||||
"",
|
||||
"接口。",
|
||||
"",
|
||||
"### `Load(mode)`",
|
||||
"",
|
||||
"声明:function",
|
||||
"",
|
||||
"加载。",
|
||||
"",
|
||||
"#### `mode` 取值",
|
||||
"",
|
||||
"#### 示例",
|
||||
]
|
||||
|
||||
entries = list(module.iter_api_entries(lines))
|
||||
|
||||
self.assertEqual(
|
||||
["Widget", "Open", "Parse", "DemoUnit", "Load"],
|
||||
[entry.heading.name for entry in entries],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import io
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -10,6 +15,7 @@ SCRIPT = (
|
||||
/ "scripts"
|
||||
/ "lookup.py"
|
||||
)
|
||||
SKILL_MD = SCRIPT.parents[1] / "SKILL.md"
|
||||
|
||||
|
||||
def load_script():
|
||||
@@ -20,6 +26,752 @@ def load_script():
|
||||
|
||||
|
||||
class LookupTest(unittest.TestCase):
|
||||
def write_unified_api_fixture(self, skill_root):
|
||||
data_dir = skill_root / "data"
|
||||
codegen_dir = skill_root / "references" / "codegen"
|
||||
data_dir.mkdir()
|
||||
(codegen_dir / "project").mkdir(parents=True)
|
||||
|
||||
header = [
|
||||
"name",
|
||||
"scope",
|
||||
"module",
|
||||
"signature",
|
||||
"page",
|
||||
"anchor",
|
||||
"tags",
|
||||
"summary",
|
||||
"kind",
|
||||
"binding",
|
||||
"visibility",
|
||||
"owner",
|
||||
"qualified_name",
|
||||
]
|
||||
|
||||
def row(
|
||||
name,
|
||||
signature,
|
||||
page,
|
||||
anchor,
|
||||
summary,
|
||||
kind,
|
||||
binding="",
|
||||
visibility="",
|
||||
owner="",
|
||||
qualified_name="",
|
||||
):
|
||||
return [
|
||||
name,
|
||||
"project",
|
||||
"demo",
|
||||
signature,
|
||||
page,
|
||||
anchor,
|
||||
"",
|
||||
summary,
|
||||
kind,
|
||||
binding,
|
||||
visibility,
|
||||
owner,
|
||||
qualified_name or name,
|
||||
]
|
||||
|
||||
rows = [
|
||||
row(
|
||||
"DemoUnit",
|
||||
"DemoUnit",
|
||||
"project/demo_unit.md",
|
||||
"demounit",
|
||||
"接口。",
|
||||
"unit",
|
||||
),
|
||||
row(
|
||||
"Open",
|
||||
"Open()",
|
||||
"project/demo_unit.md",
|
||||
"open",
|
||||
"打开。",
|
||||
"function",
|
||||
"unit",
|
||||
"public",
|
||||
"DemoUnit",
|
||||
"DemoUnit.Open",
|
||||
),
|
||||
row(
|
||||
"Document",
|
||||
"Document",
|
||||
"project/demo_unit.md",
|
||||
"document",
|
||||
"文档。",
|
||||
"class",
|
||||
"unit",
|
||||
"public",
|
||||
"DemoUnit",
|
||||
"DemoUnit.Document",
|
||||
),
|
||||
row(
|
||||
"Save",
|
||||
"Save(path)",
|
||||
"project/demo_unit.md",
|
||||
"savepath",
|
||||
"按路径保存。",
|
||||
"method",
|
||||
"instance",
|
||||
"public",
|
||||
"DemoUnit.Document",
|
||||
"DemoUnit.Document.Save",
|
||||
),
|
||||
row(
|
||||
"Save",
|
||||
"Save(mode)",
|
||||
"project/demo_unit.md",
|
||||
"savemode",
|
||||
"按模式保存。",
|
||||
"method",
|
||||
"instance",
|
||||
"protected",
|
||||
"DemoUnit.Document",
|
||||
"DemoUnit.Document.Save",
|
||||
),
|
||||
row(
|
||||
"Name",
|
||||
"Name",
|
||||
"project/demo_unit.md",
|
||||
"name",
|
||||
"文档名称。",
|
||||
"field",
|
||||
"instance",
|
||||
"public",
|
||||
"DemoUnit.Document",
|
||||
"DemoUnit.Document.Name",
|
||||
),
|
||||
row(
|
||||
"Other",
|
||||
"Other",
|
||||
"project/demo_unit.md",
|
||||
"other",
|
||||
"其他对象。",
|
||||
"class",
|
||||
"unit",
|
||||
"public",
|
||||
"DemoUnit",
|
||||
"DemoUnit.Other",
|
||||
),
|
||||
row(
|
||||
"Save",
|
||||
"Save()",
|
||||
"project/demo_unit.md",
|
||||
"save",
|
||||
"保存其他对象。",
|
||||
"method",
|
||||
"instance",
|
||||
"public",
|
||||
"DemoUnit.Other",
|
||||
"DemoUnit.Other.Save",
|
||||
),
|
||||
row(
|
||||
"Widget",
|
||||
"Widget",
|
||||
"project/widget.md",
|
||||
"widget",
|
||||
"组件。",
|
||||
"class",
|
||||
),
|
||||
row(
|
||||
"Create",
|
||||
"Create()",
|
||||
"project/widget.md",
|
||||
"create",
|
||||
"创建组件。",
|
||||
"method",
|
||||
"class",
|
||||
"public",
|
||||
"Widget",
|
||||
"Widget.Create",
|
||||
),
|
||||
row(
|
||||
"Count",
|
||||
"Count",
|
||||
"project/widget.md",
|
||||
"count",
|
||||
"组件数量。",
|
||||
"field",
|
||||
"static",
|
||||
"protected",
|
||||
"Widget",
|
||||
"Widget.Count",
|
||||
),
|
||||
]
|
||||
index_text = "\n".join(
|
||||
["\t".join(header), *("\t".join(values) for values in rows)]
|
||||
)
|
||||
tsv_path = data_dir / "function_index.tsv"
|
||||
tsv_path.write_text(index_text + "\n", encoding="utf-8")
|
||||
|
||||
(codegen_dir / "project" / "demo_unit.md").write_text(
|
||||
"# Demo / Unit\n\n"
|
||||
"## `DemoUnit`\n\n"
|
||||
"声明:unit\n\n"
|
||||
"接口。\n\n"
|
||||
"### `Open()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"打开。\n\n"
|
||||
"返回:Document\n\n"
|
||||
"### `Document`\n\n"
|
||||
"声明:class\n\n"
|
||||
"文档。\n\n"
|
||||
"#### `Save(path)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"按路径保存。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"#### `Save(mode)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"按模式保存。\n\n"
|
||||
"可见性:`protected`\n\n"
|
||||
"#### `Name`\n\n"
|
||||
"声明:field\n\n"
|
||||
"文档名称。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"类型:string\n\n"
|
||||
"### `Other`\n\n"
|
||||
"声明:class\n\n"
|
||||
"其他对象。\n\n"
|
||||
"#### `Save()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"保存其他对象。\n\n"
|
||||
"可见性:`public`\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(codegen_dir / "project" / "widget.md").write_text(
|
||||
"# Demo / Class\n\n"
|
||||
"## `Widget`\n\n"
|
||||
"声明:class\n\n"
|
||||
"组件。\n\n"
|
||||
"### `Create()`\n\n"
|
||||
"声明:class function\n\n"
|
||||
"创建组件。\n\n"
|
||||
"可见性:`public`\n\n"
|
||||
"### `Count`\n\n"
|
||||
"声明:static field\n\n"
|
||||
"组件数量。\n\n"
|
||||
"可见性:`protected`\n\n"
|
||||
"类型:integer\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return tsv_path
|
||||
|
||||
def write_mixed_api_fixture(self, skill_root):
|
||||
data_dir = skill_root / "data"
|
||||
codegen_dir = skill_root / "references" / "codegen" / "project"
|
||||
data_dir.mkdir()
|
||||
codegen_dir.mkdir(parents=True)
|
||||
header = [
|
||||
"name",
|
||||
"scope",
|
||||
"module",
|
||||
"signature",
|
||||
"page",
|
||||
"anchor",
|
||||
"tags",
|
||||
"summary",
|
||||
"kind",
|
||||
"binding",
|
||||
"visibility",
|
||||
"owner",
|
||||
"qualified_name",
|
||||
]
|
||||
page = "project/mixed.md"
|
||||
rows = [
|
||||
[
|
||||
"Widget",
|
||||
"project",
|
||||
"mixed",
|
||||
"Widget",
|
||||
page,
|
||||
"widget",
|
||||
"",
|
||||
"组件。",
|
||||
"class",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Widget",
|
||||
],
|
||||
[
|
||||
"Open",
|
||||
"project",
|
||||
"mixed",
|
||||
"Open()",
|
||||
page,
|
||||
"open",
|
||||
"",
|
||||
"打开。",
|
||||
"method",
|
||||
"instance",
|
||||
"public",
|
||||
"Widget",
|
||||
"Widget.Open",
|
||||
],
|
||||
[
|
||||
"Parse",
|
||||
"project",
|
||||
"mixed",
|
||||
"Parse(text)",
|
||||
page,
|
||||
"parse",
|
||||
"",
|
||||
"解析。",
|
||||
"function",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Parse",
|
||||
],
|
||||
[
|
||||
"Runtime",
|
||||
"project",
|
||||
"mixed",
|
||||
"Runtime",
|
||||
page,
|
||||
"runtime",
|
||||
"",
|
||||
"接口。",
|
||||
"unit",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Runtime",
|
||||
],
|
||||
[
|
||||
"Document",
|
||||
"project",
|
||||
"mixed",
|
||||
"Document",
|
||||
page,
|
||||
"document",
|
||||
"",
|
||||
"文档。",
|
||||
"class",
|
||||
"unit",
|
||||
"public",
|
||||
"Runtime",
|
||||
"Runtime.Document",
|
||||
],
|
||||
[
|
||||
"Save",
|
||||
"project",
|
||||
"mixed",
|
||||
"Save()",
|
||||
page,
|
||||
"save",
|
||||
"",
|
||||
"保存。",
|
||||
"method",
|
||||
"instance",
|
||||
"public",
|
||||
"Runtime.Document",
|
||||
"Runtime.Document.Save",
|
||||
],
|
||||
[
|
||||
"Other",
|
||||
"project",
|
||||
"mixed",
|
||||
"Other",
|
||||
page,
|
||||
"other",
|
||||
"",
|
||||
"其他组件。",
|
||||
"class",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Other",
|
||||
],
|
||||
]
|
||||
tsv_path = data_dir / "function_index.tsv"
|
||||
tsv_path.write_text(
|
||||
"\n".join(
|
||||
["\t".join(header), *("\t".join(row) for row in rows)]
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(codegen_dir / "mixed.md").write_text(
|
||||
"# Mixed\n\n"
|
||||
"## `Widget`\n\n声明:class\n\n组件。\n\n"
|
||||
"### `Open()`\n\n声明:function\n\n打开。\n\n可见性:public\n\n"
|
||||
"## `Parse(text)`\n\n声明:function\n\n解析。\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `text` | string | 文本 |\n\n"
|
||||
"返回:Widget\n\n"
|
||||
"### 示例\n\n"
|
||||
"```markdown\n## `NotBoundary`\n```\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\n"
|
||||
"## `Other`\n\n声明:class\n\n其他组件。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return tsv_path
|
||||
|
||||
def run_lookup(self, tsv_path, *args):
|
||||
return subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
*args,
|
||||
"--tsv",
|
||||
str(tsv_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_tsv_without_tags_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = Path(temp_dir) / "function_index.tsv"
|
||||
tsv_path.write_text(
|
||||
"name\tscope\tmodule\tsignature\tpage\tanchor\tsummary\n"
|
||||
"foo\tbuiltin\tmisc\tfoo()\tmisc.md\tfoo\tLegacy entry\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--kw",
|
||||
"Legacy",
|
||||
"--tsv",
|
||||
str(tsv_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertEqual("", result.stdout)
|
||||
self.assertIn("missing required columns: tags", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_missing_required_tsv_columns_are_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = Path(temp_dir) / "function_index.tsv"
|
||||
tsv_path.write_text("name\nfoo\n", encoding="utf-8")
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--name",
|
||||
"foo",
|
||||
"--tsv",
|
||||
str(tsv_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("missing required columns", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_tsv_row_width_is_validated(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = Path(temp_dir) / "function_index.tsv"
|
||||
tsv_path.write_text(
|
||||
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
||||
"foo\tbuiltin\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--name",
|
||||
"foo",
|
||||
"--tsv",
|
||||
str(tsv_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("line 2 has 2 columns; expected 8", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_missing_entry_body_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_root = Path(temp_dir)
|
||||
data_dir = skill_root / "data"
|
||||
codegen_dir = skill_root / "references" / "codegen"
|
||||
data_dir.mkdir()
|
||||
codegen_dir.mkdir(parents=True)
|
||||
(data_dir / "function_index.tsv").write_text(
|
||||
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
||||
"foo\tbuiltin\tmisc\tfoo()\tmissing.md\tfoo\t\tFallback only\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--name",
|
||||
"foo",
|
||||
"--tsv",
|
||||
str(data_dir / "function_index.tsv"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertEqual("", result.stdout)
|
||||
self.assertIn("entry body not found", result.stderr)
|
||||
self.assertIn("missing.md#foo", result.stderr)
|
||||
|
||||
def test_empty_queries_are_rejected(self):
|
||||
for option in ("--name", "--kw"):
|
||||
with self.subTest(option=option):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), option, ""],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(2, result.returncode)
|
||||
self.assertIn("must not be empty", result.stderr)
|
||||
|
||||
def test_non_positive_limit_is_rejected(self):
|
||||
for limit in ("0", "-1"):
|
||||
with self.subTest(limit=limit):
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--kw",
|
||||
"数组",
|
||||
"--limit",
|
||||
limit,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(2, result.returncode)
|
||||
self.assertIn("--limit must be >= 1", result.stderr)
|
||||
|
||||
def test_exact_search_uses_anchor_for_duplicate_signatures(self):
|
||||
module = load_script()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_root = Path(temp_dir)
|
||||
data_dir = skill_root / "data"
|
||||
codegen_dir = skill_root / "references" / "codegen"
|
||||
data_dir.mkdir()
|
||||
codegen_dir.mkdir(parents=True)
|
||||
|
||||
header = [
|
||||
"name",
|
||||
"scope",
|
||||
"module",
|
||||
"signature",
|
||||
"page",
|
||||
"anchor",
|
||||
"tags",
|
||||
"summary",
|
||||
]
|
||||
rows = [
|
||||
[
|
||||
"CAPA",
|
||||
"builtin",
|
||||
"language",
|
||||
"CAPA()",
|
||||
"objects.md",
|
||||
"capa",
|
||||
"",
|
||||
"IMAP summary",
|
||||
],
|
||||
[
|
||||
"CAPA",
|
||||
"builtin",
|
||||
"language",
|
||||
"CAPA()",
|
||||
"objects.md",
|
||||
"capa-1",
|
||||
"",
|
||||
"Pop3 summary",
|
||||
],
|
||||
]
|
||||
index_text = "\n".join(
|
||||
["\t".join(header), *("\t".join(row) for row in rows)]
|
||||
)
|
||||
(data_dir / "function_index.tsv").write_text(
|
||||
index_text + "\n", encoding="utf-8"
|
||||
)
|
||||
(codegen_dir / "objects.md").write_text(
|
||||
"# Objects\n\n"
|
||||
"## `CAPA()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"对象:IMAP\n\n"
|
||||
"## `CAPA()`\n\n"
|
||||
"声明:function\n\n"
|
||||
"对象:Pop3\n\n"
|
||||
"",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
stdout = io.StringIO()
|
||||
with redirect_stdout(stdout):
|
||||
status = module.main(
|
||||
[
|
||||
"--name",
|
||||
"CAPA",
|
||||
"--tsv",
|
||||
str(data_dir / "function_index.tsv"),
|
||||
]
|
||||
)
|
||||
|
||||
output = stdout.getvalue()
|
||||
self.assertEqual(0, status)
|
||||
self.assertEqual(1, output.count("对象:IMAP"))
|
||||
self.assertEqual(1, output.count("对象:Pop3"))
|
||||
self.assertIn("objects.md#capa -->", output)
|
||||
self.assertIn("objects.md#capa-1 -->", output)
|
||||
|
||||
def test_exact_qualified_member_query_returns_only_member_overloads(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(
|
||||
tsv_path, "--name", "demounit.document.save"
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(2, result.stdout.count("#### `Save("))
|
||||
self.assertNotIn("#### `Name`", result.stdout)
|
||||
self.assertNotIn("### `Other`", result.stdout)
|
||||
|
||||
def test_exact_unit_class_query_returns_its_h4_subtree_only(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(
|
||||
tsv_path, "--name", "DemoUnit.Document"
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("### `Document`", result.stdout)
|
||||
self.assertIn("#### `Save(path)`", result.stdout)
|
||||
self.assertIn("#### `Name`", result.stdout)
|
||||
self.assertNotIn("### `Other`", result.stdout)
|
||||
|
||||
def test_exact_unit_query_returns_the_complete_unit_subtree(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(tsv_path, "--name", "DemoUnit")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("## `DemoUnit`", result.stdout)
|
||||
self.assertIn("### `Open()`", result.stdout)
|
||||
self.assertIn("### `Document`", result.stdout)
|
||||
self.assertIn("#### `Save(path)`", result.stdout)
|
||||
self.assertIn("### `Other`", result.stdout)
|
||||
self.assertNotIn("## `Widget`", result.stdout)
|
||||
|
||||
def test_exact_class_query_returns_all_class_members(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(tsv_path, "--name", "widget")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("## `Widget`", result.stdout)
|
||||
self.assertIn("### `Create()`\n\n声明:class function", result.stdout)
|
||||
self.assertIn("### `Count`\n\n声明:static field", result.stdout)
|
||||
self.assertNotIn("## `DemoUnit`", result.stdout)
|
||||
|
||||
def test_exact_class_in_mixed_page_stops_at_next_h2(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(tsv_path, "--name", "Widget")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("### `Open()`", result.stdout)
|
||||
self.assertNotIn("## `Parse(text)`", result.stdout)
|
||||
|
||||
def test_exact_function_in_mixed_page_keeps_h3_example_only(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(tsv_path, "--name", "Parse")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("### 示例", result.stdout)
|
||||
self.assertIn("## `NotBoundary`", result.stdout)
|
||||
self.assertNotIn("## `Runtime`", result.stdout)
|
||||
|
||||
def test_exact_unit_in_mixed_page_stops_before_following_class(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(tsv_path, "--name", "Runtime")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("#### `Save()`", result.stdout)
|
||||
self.assertNotIn("## `Other`", result.stdout)
|
||||
|
||||
def test_unqualified_member_query_returns_all_owners(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
||||
|
||||
result = self.run_lookup(tsv_path, "--name", "Save")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(3, result.stdout.count("#### `Save("))
|
||||
self.assertIn("按路径保存。", result.stdout)
|
||||
self.assertIn("按模式保存。", result.stdout)
|
||||
self.assertIn("保存其他对象。", result.stdout)
|
||||
|
||||
def test_keyword_search_includes_unified_fields_and_declaration_term(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
||||
|
||||
qualified = self.run_lookup(
|
||||
tsv_path, "--kw", "Widget.Create"
|
||||
)
|
||||
visibility = self.run_lookup(
|
||||
tsv_path, "--kw", "protected", "method"
|
||||
)
|
||||
|
||||
self.assertEqual(0, qualified.returncode, qualified.stderr)
|
||||
self.assertIn("Widget.Create\tclass function\tCreate()", qualified.stdout)
|
||||
self.assertNotIn("static function", qualified.stdout)
|
||||
self.assertEqual(0, visibility.returncode, visibility.stderr)
|
||||
self.assertIn("DemoUnit.Document.Save", visibility.stdout)
|
||||
|
||||
def test_skill_documents_qualified_queries_and_legacy_index_compatibility(self):
|
||||
text = SKILL_MD.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("完全限定名称", text)
|
||||
self.assertIn("qualified_name", text)
|
||||
self.assertIn("owner、kind、binding、visibility", text)
|
||||
self.assertIn("class function", text)
|
||||
self.assertIn("旧 8 列", text)
|
||||
|
||||
def test_keyword_search_includes_tags_and_summary(self):
|
||||
module = load_script()
|
||||
rows = [
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
TOOL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = TOOL_ROOT.parents[1]
|
||||
CONVERT = TOOL_ROOT / "scripts" / "convert_tsf.py"
|
||||
GENERATE = TOOL_ROOT / "scripts" / "generate.py"
|
||||
LINT = TOOL_ROOT / "scripts" / "lint.py"
|
||||
BUILD_INDEX = TOOL_ROOT / "scripts" / "build_index.py"
|
||||
LOOKUP = REPO_ROOT / "skills" / "tsl-api-reference" / "scripts" / "lookup.py"
|
||||
STANDARD = TOOL_ROOT / "STANDARD.md"
|
||||
README = TOOL_ROOT / "README.md"
|
||||
EXAMPLE_JSON = TOOL_ROOT / "examples" / "example.json"
|
||||
EXAMPLE_YAML = TOOL_ROOT / "examples" / "example.yaml"
|
||||
SKILL = REPO_ROOT / "skills" / "tsl-api-reference" / "SKILL.md"
|
||||
|
||||
|
||||
class UnifiedPipelineTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp_dir.name)
|
||||
self.skill_dir = self.root / "skills" / "tsl-api-reference"
|
||||
|
||||
def tearDown(self):
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_command(self, *args, cwd=None):
|
||||
return subprocess.run(
|
||||
[sys.executable, *map(str, args)],
|
||||
cwd=cwd or REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def run_pipeline(self, sources, queries):
|
||||
tsf_paths = []
|
||||
for name, source in sources:
|
||||
tsf_path = self.root / f"{name}.tsf"
|
||||
tsf_path.write_text(
|
||||
textwrap.dedent(source).lstrip(), encoding="utf-8"
|
||||
)
|
||||
tsf_paths.append(tsf_path)
|
||||
|
||||
page_path = "base/mixed"
|
||||
recordings = {}
|
||||
recording_paths = {}
|
||||
for output_format in ("json", "yaml"):
|
||||
recording = self.root / f"mixed.{output_format}"
|
||||
convert = self.run_command(
|
||||
CONVERT,
|
||||
*tsf_paths,
|
||||
"--format",
|
||||
output_format,
|
||||
"--module",
|
||||
"示例 / 混合 API",
|
||||
"--path",
|
||||
page_path,
|
||||
"--output",
|
||||
recording,
|
||||
)
|
||||
self.assertEqual(0, convert.returncode, convert.stderr)
|
||||
recording_paths[output_format] = recording
|
||||
text = recording.read_text(encoding="utf-8")
|
||||
recordings[output_format] = (
|
||||
json.loads(text)
|
||||
if output_format == "json"
|
||||
else yaml.safe_load(text)
|
||||
)
|
||||
self.assertEqual(recordings["json"], recordings["yaml"])
|
||||
|
||||
markdown = (
|
||||
self.skill_dir
|
||||
/ "references"
|
||||
/ "codegen"
|
||||
/ "project"
|
||||
/ f"{page_path}.md"
|
||||
)
|
||||
generated_markdown = {}
|
||||
for output_format, recording in recording_paths.items():
|
||||
generate = self.run_command(GENERATE, recording, cwd=self.root)
|
||||
self.assertEqual(0, generate.returncode, generate.stderr)
|
||||
self.assertTrue(markdown.is_file())
|
||||
generated_markdown[output_format] = markdown.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
lint = self.run_command(LINT, "--file", markdown, "--strict")
|
||||
self.assertEqual(0, lint.returncode, lint.stdout + lint.stderr)
|
||||
self.assertEqual(
|
||||
generated_markdown["json"], generated_markdown["yaml"]
|
||||
)
|
||||
|
||||
build = self.run_command(BUILD_INDEX, "--skill-dir", self.skill_dir)
|
||||
self.assertEqual(0, build.returncode, build.stderr)
|
||||
check = self.run_command(
|
||||
BUILD_INDEX, "--skill-dir", self.skill_dir, "--check"
|
||||
)
|
||||
self.assertEqual(0, check.returncode, check.stderr)
|
||||
|
||||
lookup_outputs = {}
|
||||
for query in queries:
|
||||
lookup = self.run_command(
|
||||
LOOKUP,
|
||||
"--name",
|
||||
query,
|
||||
"--tsv",
|
||||
self.skill_dir / "data" / "function_index.tsv",
|
||||
)
|
||||
self.assertEqual(0, lookup.returncode, lookup.stderr)
|
||||
lookup_outputs[query] = lookup.stdout
|
||||
return (
|
||||
recordings["json"],
|
||||
generated_markdown["json"],
|
||||
lookup_outputs,
|
||||
)
|
||||
|
||||
@unittest.skipUnless(yaml is not None, "pyyaml is not installed")
|
||||
def test_mixed_pipeline_convert_generate_lint_index_lookup(self):
|
||||
recording, markdown, lookups = self.run_pipeline(
|
||||
sources=[
|
||||
(
|
||||
"Widget",
|
||||
"""
|
||||
type Widget = class
|
||||
/// 组件
|
||||
public
|
||||
/// 重命名组件
|
||||
/// @param: name 组件名称
|
||||
function Rename(name: string): string;
|
||||
/// 创建组件
|
||||
class function Create(): Widget;
|
||||
/// 组件标题
|
||||
property Title: string read title_ write title_;
|
||||
/// 原始标题
|
||||
property RawTitle read title_;
|
||||
/// 标题存储
|
||||
title_: string;
|
||||
/// 组件数量
|
||||
static Count: integer;
|
||||
/// 默认类型
|
||||
const DefaultKind = 1;
|
||||
/// 最大数量
|
||||
static const Maximum = 100;
|
||||
end;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"OpenWidget",
|
||||
"""
|
||||
function OpenWidget(path: string): Widget;
|
||||
begin
|
||||
/// 打开组件
|
||||
/// @param: path 组件路径
|
||||
return nil;
|
||||
end;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"DemoUnit",
|
||||
"""
|
||||
unit DemoUnit;
|
||||
/// 接口
|
||||
interface
|
||||
/// 默认大小
|
||||
const DefaultSize = 100;
|
||||
/// 当前文档
|
||||
var CurrentDocument: Document;
|
||||
/// 打开文档
|
||||
/// @param: path 文档路径
|
||||
function OpenDocument(path: string): Document;
|
||||
type Document = class
|
||||
/// 文档
|
||||
public
|
||||
/// 保存
|
||||
function Save(): boolean;
|
||||
/// 创建文档
|
||||
class function Create(): Document;
|
||||
/// 文档标题
|
||||
property Title: string read title_ write title_;
|
||||
private
|
||||
title_: string;
|
||||
end;
|
||||
implementation
|
||||
end.
|
||||
""",
|
||||
),
|
||||
],
|
||||
queries=("Widget", "OpenWidget", "DemoUnit.Document"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
["class", "function", "unit"],
|
||||
[item["kind"] for item in recording["declarations"]],
|
||||
)
|
||||
widget_members = recording["declarations"][0]["members"]
|
||||
raw_title = next(
|
||||
item for item in widget_members if item["name"] == "RawTitle"
|
||||
)
|
||||
self.assertEqual("", raw_title["type"])
|
||||
self.assertEqual([], raw_title["params"])
|
||||
self.assertIn("## `Widget`\n\n声明:class", markdown)
|
||||
self.assertIn("## `OpenWidget(path)`\n\n声明:function", markdown)
|
||||
self.assertIn("## `DemoUnit`\n\n声明:unit", markdown)
|
||||
self.assertIn(
|
||||
"### `Create()`\n\n声明:class function", lookups["Widget"]
|
||||
)
|
||||
self.assertIn(
|
||||
"### `Rename(name)`\n\n声明:function", lookups["Widget"]
|
||||
)
|
||||
self.assertNotIn("## `OpenWidget()`", lookups["Widget"])
|
||||
self.assertIn("声明:function", lookups["OpenWidget"])
|
||||
self.assertNotIn("## `DemoUnit`", lookups["OpenWidget"])
|
||||
self.assertIn(
|
||||
"#### `Save()`\n\n声明:function", lookups["DemoUnit.Document"]
|
||||
)
|
||||
self.assertIn(
|
||||
"#### `Create()`\n\n声明:class function",
|
||||
lookups["DemoUnit.Document"],
|
||||
)
|
||||
|
||||
def test_standard_and_readme_describe_unified_declarations(self):
|
||||
standard = STANDARD.read_text(encoding="utf-8")
|
||||
readme = README.read_text(encoding="utf-8")
|
||||
skill = SKILL.read_text(encoding="utf-8")
|
||||
|
||||
self.assertTrue(standard.startswith("# TSL API 文档标准\n"))
|
||||
self.assertNotIn("根字段 `functions`、`class`、`unit`", standard)
|
||||
self.assertNotIn("每个 markdown 叶子页只记录一种页面类型", standard)
|
||||
for fragment in (
|
||||
"declarations",
|
||||
"声明:function",
|
||||
"声明:class",
|
||||
"声明:unit",
|
||||
"顶级 function",
|
||||
"顶级 class",
|
||||
"顶级 unit",
|
||||
"综合示例",
|
||||
):
|
||||
self.assertIn(fragment, standard)
|
||||
for heading in (
|
||||
"### function\n",
|
||||
"### class\n",
|
||||
"### unit\n",
|
||||
"### 综合示例\n",
|
||||
):
|
||||
self.assertIn(heading, standard)
|
||||
for heading in (
|
||||
"### 顶级 function\n",
|
||||
"### 顶级 class\n",
|
||||
"### 顶级 unit\n",
|
||||
"### 混合页面示例\n",
|
||||
):
|
||||
self.assertNotIn(heading, standard)
|
||||
for fragment in (
|
||||
"declarations",
|
||||
"混合输入",
|
||||
"OpenXmlAttribute.tsf",
|
||||
"OpenXmlRuntime.tsf",
|
||||
"13 列",
|
||||
"property 类型可选",
|
||||
):
|
||||
self.assertIn(fragment, readme)
|
||||
self.assertIn(
|
||||
"## `OpenXmlAttribute`\n\n声明:class", readme
|
||||
)
|
||||
self.assertIn(
|
||||
"### `CreateVirtual(position, row_index, _story)`\n\n"
|
||||
"声明:class function",
|
||||
readme,
|
||||
)
|
||||
self.assertNotIn("property/field/variable 缺少类型", readme)
|
||||
self.assertIn("混合页面", skill)
|
||||
self.assertIn("page#anchor", skill)
|
||||
|
||||
def test_standard_class_methods_reuse_top_level_function_structure(self):
|
||||
standard = STANDARD.read_text(encoding="utf-8")
|
||||
class_section = standard.split("### class\n", 1)[1].split(
|
||||
"### unit\n", 1
|
||||
)[0]
|
||||
normalized_class = " ".join(class_section.split())
|
||||
normalized_standard = " ".join(standard.split())
|
||||
|
||||
self.assertIn("正文结构与顶级 function 相同", normalized_class)
|
||||
self.assertIn("实例 function 和 class function", normalized_class)
|
||||
self.assertIn("标题及其子标题整体下移一级", normalized_class)
|
||||
self.assertEqual(
|
||||
2,
|
||||
class_section.count("<!-- tags: OpenXml 属性 创建 -->"),
|
||||
)
|
||||
self.assertIn(
|
||||
"所有 class member 都可以包含可选的 `tags`",
|
||||
normalized_standard,
|
||||
)
|
||||
self.assertIn(
|
||||
"### `CreateVirtual(position, row_index, _story)`",
|
||||
class_section,
|
||||
)
|
||||
self.assertIn("声明:class function", class_section)
|
||||
self.assertIn("返回:ParagraphSegment", class_section)
|
||||
|
||||
@unittest.skipUnless(yaml is not None, "pyyaml is not installed")
|
||||
def test_json_and_yaml_examples_use_the_same_complete_structure(self):
|
||||
json_data = json.loads(EXAMPLE_JSON.read_text(encoding="utf-8"))
|
||||
yaml_data = yaml.safe_load(EXAMPLE_YAML.read_text(encoding="utf-8"))
|
||||
|
||||
expected_root = {"module", "path", "declarations"}
|
||||
self.assertEqual(expected_root, set(json_data))
|
||||
self.assertEqual(expected_root, set(yaml_data))
|
||||
self.assertEqual(json_data, yaml_data)
|
||||
|
||||
declarations = {
|
||||
item["kind"]: item for item in json_data["declarations"]
|
||||
}
|
||||
self.assertEqual({"function", "class", "unit"}, set(declarations))
|
||||
self.assertEqual(
|
||||
["class", "function", "unit"],
|
||||
[item["kind"] for item in json_data["declarations"]],
|
||||
)
|
||||
|
||||
class_members = declarations["class"]["members"]
|
||||
self.assertEqual(
|
||||
[
|
||||
"create",
|
||||
"CreateVirtual",
|
||||
"Prefix",
|
||||
"NamespaceUri",
|
||||
"value_",
|
||||
"Count",
|
||||
"DefaultPrefix",
|
||||
"MaximumAttributes",
|
||||
],
|
||||
[member["name"] for member in class_members],
|
||||
)
|
||||
self.assertIn("class", [m.get("binding") for m in class_members])
|
||||
self.assertEqual(
|
||||
"string",
|
||||
next(m for m in class_members if m["name"] == "Prefix")["type"],
|
||||
)
|
||||
self.assertNotIn(
|
||||
"type",
|
||||
next(m for m in class_members if m["name"] == "NamespaceUri"),
|
||||
)
|
||||
self.assertEqual(
|
||||
{"method", "property", "field", "constant"},
|
||||
{member["kind"] for member in class_members},
|
||||
)
|
||||
self.assertTrue(
|
||||
next(m for m in class_members if m["name"] == "Count")["static"]
|
||||
)
|
||||
self.assertTrue(
|
||||
next(
|
||||
m
|
||||
for m in class_members
|
||||
if m["name"] == "MaximumAttributes"
|
||||
)["static"]
|
||||
)
|
||||
|
||||
unit_members = declarations["unit"]["members"]
|
||||
self.assertEqual(
|
||||
{"constant", "variable", "function", "class"},
|
||||
{member["kind"] for member in unit_members},
|
||||
)
|
||||
document = next(
|
||||
member for member in unit_members if member["kind"] == "class"
|
||||
)
|
||||
self.assertEqual(
|
||||
{("method", "instance"), ("method", "class"), ("property", None)},
|
||||
{(m["kind"], m.get("binding")) for m in document["members"]},
|
||||
)
|
||||
|
||||
page = (
|
||||
self.skill_dir
|
||||
/ "references"
|
||||
/ "codegen"
|
||||
/ "project"
|
||||
/ f"{json_data['path']}.md"
|
||||
)
|
||||
markdown_by_format = {}
|
||||
for output_format, recording in (
|
||||
("json", EXAMPLE_JSON),
|
||||
("yaml", EXAMPLE_YAML),
|
||||
):
|
||||
generate = self.run_command(GENERATE, recording, cwd=self.root)
|
||||
self.assertEqual(0, generate.returncode, generate.stderr)
|
||||
markdown_by_format[output_format] = page.read_bytes()
|
||||
lint = self.run_command(LINT, "--file", page, "--strict")
|
||||
self.assertEqual(0, lint.returncode, lint.stdout + lint.stderr)
|
||||
self.assertEqual(markdown_by_format["json"], markdown_by_format["yaml"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user