835 lines
26 KiB
Python
835 lines
26 KiB
Python
import importlib.util
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT = Path(__file__).parents[1] / "scripts" / "lint.py"
|
|
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):
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp_dir.name)
|
|
self.page = self.root / "base" / "demo.md"
|
|
self.page.parent.mkdir()
|
|
self.page.write_text(VALID_PAGE, encoding="utf-8")
|
|
|
|
def tearDown(self):
|
|
self.temp_dir.cleanup()
|
|
|
|
def run_cli(self, *args):
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPT), *map(str, args)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def test_accepts_one_markdown_file(self):
|
|
result = self.run_cli("--file", self.page)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("1 files", result.stderr)
|
|
|
|
def test_accepts_one_directory(self):
|
|
result = self.run_cli("--dir", self.root)
|
|
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()
|