1234 lines
42 KiB
Python
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()
|