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

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

1234 lines
42 KiB
Python

import importlib.util
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPT = Path(__file__).parents[1] / "scripts" / "generate.py"
REPO_ROOT = Path(__file__).resolve().parents[3]
def load_script():
spec = importlib.util.spec_from_file_location("tsl_codegen_generate", SCRIPT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class DocGenCliTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.input = self.root / "entry.json"
self.input.write_text(
json.dumps(
{
"module": "项目 / 示例",
"path": "base/my_functions",
"declarations": [
{
"kind": "function",
"name": "demo",
"signature": "demo()",
"desc": "示例函数。",
"returns": "nil",
}
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
def tearDown(self):
self.temp_dir.cleanup()
def run_cli(self, *args, env=None):
return subprocess.run(
[sys.executable, str(SCRIPT), str(self.input), *args],
capture_output=True,
text=True,
encoding="utf-8",
cwd=self.root,
env=env,
)
def write_input(self, data):
self.input.write_text(
json.dumps(data, ensure_ascii=False),
encoding="utf-8",
)
def generated(self, relative, scope="project"):
return (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ scope
/ f"{relative}.md"
)
def write_class_member_input(self, member, relative):
self.write_input(
{
"module": "示例 / 类",
"path": relative,
"declarations": [
{
"kind": "class",
"name": "StrictClass",
"desc": "严格类。",
"members": [member],
}
],
}
)
def assert_rejected_without_overwrite(self, relative, expected):
output = self.generated(relative)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli()
self.assertEqual(1, result.returncode)
self.assertIn(expected, result.stderr)
self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))
def test_default_scope_writes_configured_path_under_project(self):
result = self.run_cli()
output = (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ "project"
/ "base"
/ "my_functions.md"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(output.is_file())
self.assertTrue(
output.read_text(encoding="utf-8").startswith("# 项目 / 示例\n")
)
def test_custom_scope_changes_first_destination_directory(self):
result = self.run_cli("--scope", "my-project")
output = (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ "my-project"
/ "base"
/ "my_functions.md"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(output.is_file())
def test_output_option_is_rejected(self):
result = self.run_cli("--output", str(self.root / "out.md"))
self.assertNotEqual(result.returncode, 0)
self.assertIn("unrecognized arguments: --output", result.stderr)
def test_generated_markdown_is_formatted_by_repo_prettier(self):
self.input.write_text(
json.dumps(
{
"module": "项目 / 示例",
"path": "base/formatted",
"declarations": [
{
"kind": "function",
"name": "demo",
"signature": "demo(short, long_name)",
"desc": "示例函数。",
"params": [
{
"name": "short",
"type": "integer",
"desc": "短说明",
},
{
"name": "long_name",
"type": "very_long_type",
"desc": "这是较长的说明",
},
],
"returns": "integer",
}
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
output = (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ "project"
/ "base"
/ "formatted.md"
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
generated = output.read_text(encoding="utf-8")
prettier = subprocess.run(
[
shutil.which("npx"),
"--no-install",
"prettier",
"--config",
str(REPO_ROOT / ".prettierrc.json"),
"--parser",
"markdown",
],
input=generated,
capture_output=True,
text=True,
encoding="utf-8",
cwd=REPO_ROOT,
)
self.assertEqual(0, prettier.returncode, prettier.stderr)
self.assertEqual(prettier.stdout, generated)
def test_missing_prettier_fails_without_writing_markdown(self):
output = (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ "project"
/ "base"
/ "my_functions.md"
)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("原内容\n", encoding="utf-8")
env = os.environ.copy()
env["PATH"] = ""
result = self.run_cli(env=env)
self.assertEqual(1, result.returncode)
self.assertIn("未找到 Prettier", result.stderr)
self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))
def test_prettier_process_failure_preserves_existing_markdown(self):
output = self.generated("base/my_functions")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("原内容\n", encoding="utf-8")
bin_dir = self.root / "bin"
bin_dir.mkdir()
fake_npx = bin_dir / "npx"
fake_npx.write_text(
"#!/bin/sh\necho formatter-failed >&2\nexit 9\n",
encoding="utf-8",
)
fake_npx.chmod(0o755)
env = os.environ.copy()
env["PATH"] = str(bin_dir)
result = self.run_cli(env=env)
self.assertEqual(1, result.returncode)
self.assertIn("Prettier 格式化失败", result.stderr)
self.assertIn("formatter-failed", result.stderr)
self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))
def test_mixed_declarations_render_typed_h2_in_input_order(self):
self.write_input(
{
"module": "示例 / 混合",
"path": "base/mixed",
"declarations": [
{
"kind": "class",
"name": "Widget",
"desc": "组件。",
"members": [],
},
{
"kind": "function",
"name": "OpenWidget",
"signature": "OpenWidget()",
"desc": "打开组件。",
"returns": "Widget",
},
{
"kind": "unit",
"name": "WidgetRuntime",
"desc": "运行时接口。",
"members": [],
},
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/mixed").read_text(encoding="utf-8")
markers = [
"## `Widget`\n\n声明:class\n\n组件。",
"## `OpenWidget()`\n\n声明:function\n\n打开组件。",
"## `WidgetRuntime`\n\n声明:unit\n\n运行时接口。",
]
positions = [text.index(marker) for marker in markers]
self.assertEqual(sorted(positions), positions)
def test_class_page_renders_binding_visibility_and_member_order(self):
self.write_input(
{
"module": "示例 / 类",
"path": "base/widget",
"declarations": [
{
"kind": "class",
"name": "Widget",
"desc": "表示组件。",
"tags": ["组件", "示例"],
"bases": ["BaseWidget"],
"members": [
{
"kind": "method",
"name": "Close",
"visibility": "public",
"binding": "instance",
"signature": "Close()",
"desc": "关闭组件。",
},
{
"kind": "method",
"name": "Create",
"visibility": "protected",
"binding": "class",
"signature": "Create(name)",
"desc": "创建组件。",
"params": [
{
"name": "name",
"type": "string",
"desc": "组件名称",
}
],
"returns": "Widget",
"modifiers": ["overload"],
},
{
"kind": "property",
"name": "Title",
"visibility": "public",
"desc": "组件标题。",
"type": "string",
"access": "readwrite",
},
{
"kind": "field",
"name": "Count",
"visibility": "protected",
"desc": "组件数量。",
"type": "integer",
"static": True,
},
{
"kind": "constant",
"name": "DefaultName",
"visibility": "public",
"desc": "默认名称。",
"value": "'widget'",
},
],
}
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/widget").read_text(encoding="utf-8")
expected = [
"## `Widget`",
"声明:class",
"父类:`BaseWidget`",
"### `Close()`\n\n声明:function",
"### `Create(name)`\n\n声明:class function",
"### `Title`\n\n声明:property",
"### `Count`\n\n声明:static field",
"### `DefaultName`\n\n声明:const",
]
positions = [text.index(fragment) for fragment in expected]
self.assertEqual(sorted(positions), positions)
self.assertIn("可见性:`protected`", text)
self.assertIn("修饰符:`overload`", text)
self.assertIn("访问:read / write", text)
self.assertNotIn("static function", text)
self.assertEqual(
"# 示例 / 类\n\n"
"## `Widget`\n\n"
"声明:class\n\n"
"表示组件。\n\n"
"<!-- tags: 组件 示例 -->\n\n"
"父类:`BaseWidget`\n\n"
"### `Close()`\n\n"
"声明:function\n\n"
"关闭组件。\n\n"
"可见性:`public`\n\n"
"### `Create(name)`\n\n"
"声明:class function\n\n"
"创建组件。\n\n"
"可见性:`protected`\n\n"
"修饰符:`overload`\n\n"
"| 参数 | 类型 | 说明 |\n"
"| ------ | ------ | -------- |\n"
"| `name` | string | 组件名称 |\n\n"
"返回:Widget\n\n"
"### `Title`\n\n"
"声明:property\n\n"
"组件标题。\n\n"
"可见性:`public`\n\n"
"类型:string\n\n"
"访问:read / write\n\n"
"### `Count`\n\n"
"声明:static field\n\n"
"组件数量。\n\n"
"可见性:`protected`\n\n"
"类型:integer\n\n"
"### `DefaultName`\n\n"
"声明:const\n\n"
"默认名称。\n\n"
"可见性:`public`\n\n"
"值:`'widget'`\n",
text,
)
def test_unit_page_renders_interface_class_members_at_h4(self):
self.write_input(
{
"module": "示例 / Unit",
"path": "base/demo_unit",
"declarations": [
{
"kind": "unit",
"name": "DemoUnit",
"desc": "提供文档能力。",
"members": [
{
"kind": "constant",
"name": "DefaultSize",
"desc": "默认大小。",
"value": 100,
},
{
"kind": "variable",
"name": "CurrentDocument",
"desc": "当前文档。",
"type": "Document",
},
{
"kind": "function",
"name": "OpenDocument",
"signature": "OpenDocument(path)",
"desc": "打开文档。",
"params": [
{
"name": "path",
"type": "string",
"desc": "文档路径",
}
],
"returns": "Document",
},
{
"kind": "class",
"name": "Document",
"desc": "文档对象。",
"bases": ["BaseDocument"],
"members": [
{
"kind": "method",
"name": "Save",
"visibility": "public",
"binding": "instance",
"signature": "Save()",
"desc": "保存文档。",
"returns": "boolean",
}
],
},
],
}
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/demo_unit").read_text(encoding="utf-8")
self.assertIn("## `DemoUnit`", text)
self.assertIn("声明:unit", text)
self.assertIn("### `DefaultSize`\n\n声明:const", text)
self.assertIn("值:`100`", text)
self.assertIn("### `CurrentDocument`\n\n声明:var", text)
self.assertIn("### `OpenDocument(path)`\n\n声明:function", text)
self.assertIn("### `Document`\n\n声明:class", text)
self.assertIn("父类:`BaseDocument`", text)
self.assertIn("#### `Save()`\n\n声明:function", text)
self.assertNotIn("可见性:`public`\n\n### `Document`", text)
self.assertEqual(
"# 示例 / Unit\n\n"
"## `DemoUnit`\n\n"
"声明:unit\n\n"
"提供文档能力。\n\n"
"### `DefaultSize`\n\n"
"声明:const\n\n"
"默认大小。\n\n"
"值:`100`\n\n"
"### `CurrentDocument`\n\n"
"声明:var\n\n"
"当前文档。\n\n"
"类型:Document\n\n"
"### `OpenDocument(path)`\n\n"
"声明:function\n\n"
"打开文档。\n\n"
"| 参数 | 类型 | 说明 |\n"
"| ------ | ------ | -------- |\n"
"| `path` | string | 文档路径 |\n\n"
"返回:Document\n\n"
"### `Document`\n\n"
"声明:class\n\n"
"文档对象。\n\n"
"父类:`BaseDocument`\n\n"
"#### `Save()`\n\n"
"声明:function\n\n"
"保存文档。\n\n"
"可见性:`public`\n\n"
"返回:boolean\n",
text,
)
def test_class_method_empty_returns_is_treated_as_omitted(self):
self.write_input(
{
"module": "示例 / 类",
"path": "base/no_return",
"declarations": [
{
"kind": "class",
"name": "NoReturn",
"desc": "无返回类。",
"members": [
{
"kind": "method",
"name": "Close",
"visibility": "public",
"binding": "instance",
"signature": "Close()",
"desc": "关闭。",
"returns": "",
}
],
}
],
}
)
class_result = self.run_cli()
self.assertEqual(0, class_result.returncode, class_result.stderr)
class_text = self.generated("base/no_return").read_text(encoding="utf-8")
self.assertNotIn("返回:", class_text)
def test_unit_function_requires_returns(self):
self.write_input(
{
"module": "示例 / Unit",
"path": "base/missing_return",
"declarations": [
{
"kind": "unit",
"name": "MissingReturn",
"desc": "缺少返回。",
"members": [
{
"kind": "function",
"name": "Open",
"signature": "Open()",
"desc": "打开。",
}
],
}
],
}
)
result = self.run_cli()
self.assertEqual(1, result.returncode)
self.assertIn("missing 'returns'", result.stderr)
def test_examples_list_renders_independent_fences_and_output_comments(self):
self.write_input(
{
"module": "项目 / 示例",
"path": "base/examples",
"declarations": [
{
"kind": "function",
"name": "demo",
"signature": "demo()",
"desc": "示例函数。",
"returns": "string",
"examples": [
{
"desc": "单行输出",
"code": "return demo();",
"output": "ok",
},
{
"desc": "多行输出",
"code": "return demo();",
"output": "first\nsecond",
},
],
}
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/examples").read_text(encoding="utf-8")
self.assertEqual(2, text.count("```tsl"))
self.assertIn("范例01:单行输出", text)
self.assertIn("// 输出:ok", text)
self.assertIn("范例02:多行输出", text)
self.assertIn("// 输出:\n// first\n// second", text)
def test_atomic_write_failure_preserves_existing_output_and_cleans_temp(self):
module = load_script()
output = self.root / "existing.md"
output.write_text("原内容\n", encoding="utf-8")
with mock.patch.object(module.os, "replace", side_effect=OSError("replace failed")):
with self.assertRaisesRegex(OSError, "replace failed"):
module.atomic_write(output, "新内容\n")
self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))
self.assertFalse(any(path.suffix == ".tmp" for path in self.root.iterdir()))
def test_legacy_root_keys_are_rejected_without_overwrite(self):
legacy_values = {
"functions": [
{"signature": "Old()", "desc": "旧函数。", "returns": "nil"}
],
"class": {"name": "Old", "desc": "旧类。", "members": []},
"unit": {"name": "Old", "desc": "旧接口。", "members": []},
}
for key, value in legacy_values.items():
with self.subTest(key=key):
relative = f"base/legacy_{key}"
self.write_input(
{
"module": "示例 / 旧录入",
"path": relative,
key: value,
}
)
self.assert_rejected_without_overwrite(
relative, f"unknown field(s): {key}"
)
def test_unknown_class_member_kind_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "event",
"name": "Changed",
"visibility": "public",
"desc": "发生变化。",
},
"base/unknown_kind",
)
self.assert_rejected_without_overwrite(
"base/unknown_kind", "unknown kind 'event'"
)
def test_method_static_field_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "method",
"name": "Bad",
"visibility": "public",
"binding": "instance",
"signature": "Bad()",
"desc": "非法方法。",
"static": True,
},
"base/method_static",
)
self.assert_rejected_without_overwrite(
"base/method_static", "unknown field(s): static"
)
def test_private_class_member_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "field",
"name": "hidden_",
"visibility": "private",
"desc": "私有字段。",
"type": "string",
},
"base/private_member",
)
self.assert_rejected_without_overwrite(
"base/private_member", "visibility must be public or protected"
)
def test_property_empty_type_is_treated_as_omitted(self):
self.write_class_member_input(
{
"kind": "property",
"name": "Title",
"visibility": "public",
"desc": "标题。",
"type": "",
"access": "read",
},
"base/property_type",
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/property_type").read_text(encoding="utf-8")
self.assertIn("### `Title`\n\n声明:property", text)
self.assertIn("访问:read", text)
self.assertNotIn("类型:", text)
def test_constant_empty_type_is_treated_as_omitted(self):
self.write_class_member_input(
{
"kind": "constant",
"name": "DefaultSize",
"visibility": "public",
"desc": "默认大小。",
"type": "",
"value": 100,
},
"base/constant_type",
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/constant_type").read_text(encoding="utf-8")
self.assertIn("值:`100`", text)
self.assertNotIn("类型:", text)
def test_field_missing_type_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "field",
"name": "Count",
"visibility": "public",
"desc": "数量。",
},
"base/field_type",
)
self.assert_rejected_without_overwrite(
"base/field_type", "members[0]: type: must be non-empty"
)
def test_unit_variable_missing_type_is_rejected_without_overwrite(self):
self.write_input(
{
"module": "示例 / Unit",
"path": "base/variable_type",
"declarations": [
{
"kind": "unit",
"name": "StrictUnit",
"desc": "严格接口。",
"members": [
{
"kind": "variable",
"name": "Current",
"desc": "当前值。",
}
],
}
],
}
)
self.assert_rejected_without_overwrite(
"base/variable_type", "members[0]: type: must be non-empty"
)
def test_invalid_method_binding_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "method",
"name": "Bad",
"visibility": "public",
"binding": "static",
"signature": "Bad()",
"desc": "非法绑定。",
},
"base/invalid_binding",
)
self.assert_rejected_without_overwrite(
"base/invalid_binding", "invalid binding"
)
def test_invalid_property_access_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "property",
"name": "Title",
"visibility": "public",
"desc": "标题。",
"type": "string",
"access": "readonly",
},
"base/invalid_access",
)
self.assert_rejected_without_overwrite(
"base/invalid_access", "invalid access"
)
def test_invalid_method_modifier_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "method",
"name": "Bad",
"visibility": "public",
"binding": "instance",
"signature": "Bad()",
"desc": "非法修饰符。",
"modifiers": ["final"],
},
"base/invalid_modifier",
)
self.assert_rejected_without_overwrite(
"base/invalid_modifier", "invalid modifier"
)
def test_property_examples_are_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "property",
"name": "Title",
"visibility": "public",
"desc": "标题。",
"type": "string",
"access": "read",
"examples": [
{
"desc": "读取标题",
"code": "return Widget.Title;",
}
],
},
"base/property_examples",
)
self.assert_rejected_without_overwrite(
"base/property_examples", "unknown field(s): examples"
)
def test_unit_rejects_implementation_data_without_overwrite(self):
self.write_input(
{
"module": "示例 / Unit",
"path": "base/unit_implementation",
"declarations": [
{
"kind": "unit",
"name": "StrictUnit",
"desc": "严格接口。",
"members": [],
"implementation": ["hidden_"],
}
],
}
)
self.assert_rejected_without_overwrite(
"base/unit_implementation", "unknown field(s): implementation"
)
def test_empty_declarations_are_rejected_without_overwrite(self):
self.write_input(
{
"module": "示例 / 空页",
"path": "base/missing_branch",
"declarations": [],
}
)
self.assert_rejected_without_overwrite(
"base/missing_branch", "declarations must be a non-empty list"
)
def test_unknown_declaration_kind_is_rejected_without_overwrite(self):
self.write_input(
{
"module": "示例 / 未知声明",
"path": "base/unknown_declaration",
"declarations": [{"kind": "procedure"}],
}
)
self.assert_rejected_without_overwrite(
"base/unknown_declaration", "unknown kind 'procedure'"
)
def test_top_level_function_requires_name(self):
self.write_input(
{
"module": "示例 / 函数",
"path": "base/missing_function_name",
"declarations": [
{
"kind": "function",
"signature": "Open()",
"desc": "打开。",
"returns": "nil",
}
],
}
)
self.assert_rejected_without_overwrite(
"base/missing_function_name", "declarations[0]: name"
)
def test_top_level_function_name_must_match_signature(self):
self.write_input(
{
"module": "示例 / 函数",
"path": "base/function_name_mismatch",
"declarations": [
{
"kind": "function",
"name": "Open",
"signature": "Close()",
"desc": "关闭。",
"returns": "nil",
}
],
}
)
self.assert_rejected_without_overwrite(
"base/function_name_mismatch", "name and signature differ"
)
def test_duplicate_function_signature_is_rejected(self):
self.write_input(
{
"module": "示例 / 重复函数",
"path": "base/duplicate_function",
"declarations": [
{
"kind": "function",
"name": "Open",
"signature": "Open()",
"desc": "打开。",
"returns": "nil",
},
{
"kind": "function",
"name": "open",
"signature": "open()",
"desc": "再次打开。",
"returns": "nil",
},
],
}
)
self.assert_rejected_without_overwrite(
"base/duplicate_function", "duplicate function signature"
)
def test_duplicate_class_and_unit_names_are_rejected(self):
cases = {
"class": [
{
"kind": "class",
"name": "Widget",
"desc": "组件。",
"members": [],
},
{
"kind": "class",
"name": "widget",
"desc": "另一组件。",
"members": [],
},
],
"unit": [
{
"kind": "unit",
"name": "Runtime",
"desc": "运行时。",
"members": [],
},
{
"kind": "unit",
"name": "runtime",
"desc": "另一运行时。",
"members": [],
},
],
}
for kind, declarations in cases.items():
with self.subTest(kind=kind):
relative = f"base/duplicate_{kind}"
self.write_input(
{
"module": "示例 / 重复声明",
"path": relative,
"declarations": declarations,
}
)
self.assert_rejected_without_overwrite(
relative, f"duplicate {kind} name"
)
def test_function_overloads_and_cross_kind_same_name_are_allowed(self):
self.write_input(
{
"module": "示例 / 重载",
"path": "base/overloads",
"declarations": [
{
"kind": "function",
"name": "Open",
"signature": "Open(path)",
"desc": "按路径打开。",
"params": [
{"name": "path", "type": "string", "desc": "路径"}
],
"returns": "nil",
},
{
"kind": "function",
"name": "Open",
"signature": "Open(mode)",
"desc": "按模式打开。",
"params": [
{"name": "mode", "type": "integer", "desc": "模式"}
],
"returns": "nil",
},
{
"kind": "class",
"name": "Open",
"desc": "打开器。",
"members": [],
},
{
"kind": "unit",
"name": "Open",
"desc": "打开接口。",
"members": [],
},
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/overloads").read_text(encoding="utf-8")
markers = [
"## `Open(path)`",
"## `Open(mode)`",
"## `Open`",
"## `Open`",
]
positions = []
start = 0
for marker in markers:
position = text.index(marker, start)
positions.append(position)
start = position + len(marker)
self.assertEqual(sorted(positions), positions)
self.assertEqual(2, text.count("声明:function"))
self.assertEqual(1, text.count("声明:class"))
self.assertEqual(1, text.count("声明:unit"))
def test_constant_missing_value_is_rejected_without_overwrite(self):
self.write_class_member_input(
{
"kind": "constant",
"name": "Missing",
"visibility": "public",
"desc": "缺少值。",
},
"base/missing_value",
)
self.assert_rejected_without_overwrite(
"base/missing_value", "missing 'value'"
)
def test_class_missing_description_is_rejected_without_overwrite(self):
self.write_input(
{
"module": "示例 / 类",
"path": "base/missing_class_desc",
"declarations": [
{
"kind": "class",
"name": "MissingDescription",
"desc": "",
"members": [],
}
],
}
)
self.assert_rejected_without_overwrite(
"base/missing_class_desc",
"declarations[0]: desc: must be non-empty",
)
def test_method_name_and_parameter_order_must_match_signature(self):
cases = {
"name": {
"kind": "method",
"name": "Expected",
"visibility": "public",
"binding": "instance",
"signature": "Actual()",
"desc": "名称不一致。",
},
"parameter_order": {
"kind": "method",
"name": "Open",
"visibility": "public",
"binding": "instance",
"signature": "Open(first, second)",
"desc": "参数顺序不一致。",
"params": [
{"name": "second", "type": "integer", "desc": "第二项"},
{"name": "first", "type": "integer", "desc": "第一项"},
],
},
}
for name, member in cases.items():
with self.subTest(name=name):
relative = f"base/mismatch_{name}"
self.write_class_member_input(member, relative)
expected = "differ" if name == "name" else "signature order"
self.assert_rejected_without_overwrite(relative, expected)
def test_instance_field_and_static_constant_render_distinct_headings(self):
self.write_input(
{
"module": "示例 / 类",
"path": "base/bindings",
"declarations": [
{
"kind": "class",
"name": "Bindings",
"desc": "绑定示例。",
"members": [
{
"kind": "field",
"name": "Name",
"visibility": "public",
"desc": "名称。",
"type": "string",
},
{
"kind": "constant",
"name": "Maximum",
"visibility": "protected",
"desc": "最大值。",
"value": 10,
"static": True,
},
],
}
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/bindings").read_text(encoding="utf-8")
self.assertIn("### `Name`\n\n声明:field", text)
self.assertIn("### `Maximum`\n\n声明:static const", text)
def test_constants_accept_zero_and_false_values(self):
self.write_input(
{
"module": "示例 / Unit",
"path": "base/constants",
"declarations": [
{
"kind": "unit",
"name": "Constants",
"desc": "常量接口。",
"members": [
{
"kind": "constant",
"name": "Zero",
"desc": "零。",
"value": 0,
},
{
"kind": "constant",
"name": "Disabled",
"desc": "关闭。",
"value": False,
},
],
}
],
}
)
result = self.run_cli()
self.assertEqual(0, result.returncode, result.stderr)
text = self.generated("base/constants").read_text(encoding="utf-8")
self.assertIn("值:`0`", text)
self.assertIn("值:`false`", text)
if __name__ == "__main__":
unittest.main()