Files
playbook/tools/tsl-codegen/tests/test_convert_tsf.py
T
2026-07-30 01:23:00 +00:00

2042 lines
66 KiB
Python

import importlib.util
import json
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
from unittest import mock
SCRIPT = Path(__file__).parents[1] / "scripts" / "convert_tsf.py"
def load_script():
spec = importlib.util.spec_from_file_location(
"tsl_codegen_convert_tsf", SCRIPT
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class ConvertTsfCliTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.output = self.root / "entry.json"
def tearDown(self):
self.temp_dir.cleanup()
def write_tsf(self, name, source):
path = self.root / f"{name}.tsf"
path.write_text(textwrap.dedent(source).lstrip(), encoding="utf-8")
return path
def write_nested_tsf(self, directory, name, source):
path = self.root / directory / f"{name}.tsf"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(textwrap.dedent(source).lstrip(), encoding="utf-8")
return path
def run_cli(
self,
*inputs,
fmt="json",
output=None,
module="示例 / 函数",
path="base/example",
):
output = output or self.output
return subprocess.run(
[
sys.executable,
str(SCRIPT),
*map(str, inputs),
"--format",
fmt,
"--module",
module,
"--path",
path,
"--output",
str(output),
],
capture_output=True,
text=True,
encoding="utf-8",
)
def read_declarations(self):
return json.loads(self.output.read_text(encoding="utf-8"))[
"declarations"
]
def read_declaration(self):
declarations = self.read_declarations()
self.assertEqual(1, len(declarations))
return declarations[0]
def test_json_converts_complete_function_document(self):
source = self.write_tsf(
"Normalize",
"""
function Normalize(mode: integer = 0): integer;
begin
/// 按指定模式处理并返回模式值。
/// 第二行说明。
/// @tags: 示例 枚举
/// @param: mode {INTEGER} 处理模式,默认 0
/// @values: mode
/// 0: 原样返回
/// 1: 去重
/// "auto": 自动判断
/// @returns: INTEGER
/// @example: 使用默认模式
/// return Normalize();
/// @output:
/// 0
/// @example: 指定模式
/// return Normalize(1);
return mode;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", result.stdout)
self.assertIn(f"已写入 {self.output}", result.stderr)
self.assertEqual(
{
"module": "示例 / 函数",
"path": "base/example",
"declarations": [
{
"kind": "function",
"name": "Normalize",
"signature": "Normalize(mode)",
"desc": "按指定模式处理并返回模式值。\n第二行说明。",
"tags": ["示例", "枚举"],
"params": [
{
"name": "mode",
"type": "integer",
"optional": True,
"desc": "处理模式,默认 0",
"values": [
{"value": 0, "desc": "原样返回"},
{"value": 1, "desc": "去重"},
{"value": "auto", "desc": "自动判断"},
],
}
],
"returns": "integer",
"examples": [
{
"desc": "使用默认模式",
"code": "return Normalize();",
"output": "0",
},
{
"desc": "指定模式",
"code": "return Normalize(1);",
},
],
}
],
},
json.loads(self.output.read_text(encoding="utf-8")),
)
def test_documented_parameter_types_fill_untyped_signature(self):
source = self.write_tsf(
"DocumentedTypes",
"""
function DocumentedTypes(x, y): integer;
begin
/// 补充无类型签名的参数类型
/// @param: x {integer} 第一个值
/// @param: y {array of integer} 第二个值
return x;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
[
{"name": "x", "type": "integer", "desc": "第一个值"},
{
"name": "y",
"type": "array of integer",
"desc": "第二个值",
},
],
self.read_declaration()["params"],
)
def test_declared_parameter_types_use_colon_and_semicolon(self):
source = self.write_tsf(
"DeclaredTypes",
"""
function DeclaredTypes(x: integer; y: array of integer): integer;
begin
/// 读取签名中的参数类型
/// @param: x 第一个值
/// @param: y 第二个值
return x;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
[
{"name": "x", "type": "integer", "desc": "第一个值"},
{
"name": "y",
"type": "array of integer",
"desc": "第二个值",
},
],
self.read_declaration()["params"],
)
def test_matching_documented_parameter_type_keeps_declared_spelling(self):
source = self.write_tsf(
"MatchingParamType",
"""
function MatchingParamType(value: Array Of Integer): integer;
begin
/// 校验重复记录的参数类型
/// @param: value {array of integer} 输入值
return 1;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
[
{
"name": "value",
"type": "Array Of Integer",
"desc": "输入值",
}
],
self.read_declaration()["params"],
)
def test_parameter_type_mismatch_creates_structural_draft(self):
source = self.write_tsf(
"ParamTypeMismatch",
"""
function ParamTypeMismatch(value: integer): integer;
begin
/// 校验参数类型
/// @param: value {string} 输入值
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("", function["desc"])
self.assertEqual(
[{"name": "value", "type": "integer", "desc": ""}],
function["params"],
)
self.assertEqual("integer", function["returns"])
def test_malformed_parameter_docs_create_structural_draft(self):
cases = {
"EmptyParamType": "/// @param: value {} 输入值",
"UnclosedParamType": "/// @param: value {integer 输入值",
"MissingParamDescription": "/// @param: value {integer}",
}
for name, param_line in cases.items():
with self.subTest(name=name):
source = self.write_tsf(
name,
f"""
function {name}(value): integer;
begin
/// 校验参数类型格式
{param_line}
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("", function["desc"])
self.assertEqual(
[{"name": "value", "type": "", "desc": ""}],
function["params"],
)
self.assertEqual("integer", function["returns"])
def test_multiple_tsf_files_are_merged_in_input_order(self):
first = self.write_tsf(
"First",
"""
function First(): integer;
begin
/// 第一个函数。
return 1;
end;
""",
)
second = self.write_tsf(
"Second",
"""
function Second(): string;
begin
/// 第二个函数。
return "second";
end;
""",
)
result = self.run_cli(first, second)
self.assertEqual(0, result.returncode, result.stderr)
data = json.loads(self.output.read_text(encoding="utf-8"))
self.assertEqual(
["First()", "Second()"],
[
function["signature"]
for function in data["declarations"]
],
)
def test_mixed_function_class_unit_inputs_preserve_cli_order(self):
cls = self.write_tsf(
"Widget",
"""
type Widget = class
/// 组件。
end;
""",
)
function = self.write_tsf(
"OpenWidget",
"""
function OpenWidget(): Widget;
begin
/// 打开组件。
return nil;
end;
""",
)
unit = self.write_tsf(
"WidgetRuntime",
"""
unit WidgetRuntime;
/// 运行时接口。
interface
implementation
end.
""",
)
result = self.run_cli(cls, function, unit)
self.assertEqual(0, result.returncode, result.stderr)
declarations = json.loads(
self.output.read_text(encoding="utf-8")
)["declarations"]
self.assertEqual(
[
("class", "Widget"),
("function", "OpenWidget"),
("unit", "WidgetRuntime"),
],
[(item["kind"], item["name"]) for item in declarations],
)
def test_yaml_serialization_uses_same_data_structure(self):
try:
import yaml
except ImportError:
self.skipTest("未安装 pyyaml")
source = self.write_tsf(
"Describe",
"""
function Describe(value: string);
begin
/// 返回输入内容。
/// @param: value 输入内容
/// @returns: string
return value;
end;
""",
)
output = self.root / "entry.yaml"
result = self.run_cli(source, fmt="yaml", output=output)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
{
"module": "示例 / 函数",
"path": "base/example",
"declarations": [
{
"kind": "function",
"name": "Describe",
"signature": "Describe(value)",
"desc": "返回输入内容。",
"params": [
{
"name": "value",
"type": "string",
"desc": "输入内容",
}
],
"returns": "string",
}
],
},
yaml.safe_load(output.read_text(encoding="utf-8")),
)
def test_gb18030_encoded_tsf_is_supported(self):
source = self.root / "Legacy.tsf"
source.write_bytes(textwrap.dedent("""
function Legacy(): string;
begin
/// 返回中文内容。
return "中文";
end;
""").lstrip().encode("gb18030"))
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("返回中文内容。", function["desc"])
def test_utf8_bom_encoded_tsf_is_supported(self):
source = self.root / "BomDemo.tsf"
source.write_text(
"function BomDemo(): integer;\n"
"begin\n"
" /// 带 BOM 的说明。\n"
" return 1;\n"
"end;\n",
encoding="utf-8-sig",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("带 BOM 的说明。", function["desc"])
def test_output_must_not_overwrite_an_input_tsf(self):
source = self.write_tsf(
"KeepSource",
"""
function KeepSource(): integer;
begin
/// 保留源文件。
return 1;
end;
""",
)
original = source.read_text(encoding="utf-8")
result = self.run_cli(source, output=source)
self.assertEqual(1, result.returncode)
self.assertIn("输出文件不能覆盖输入 tsf", result.stderr)
self.assertEqual(original, source.read_text(encoding="utf-8"))
def test_atomic_write_failure_preserves_existing_output_and_cleans_temp(self):
module = load_script()
self.assertTrue(
hasattr(module, "atomic_write"), "atomic_write is missing"
)
self.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(self.output, "新内容\n")
self.assertEqual(
"原内容\n", self.output.read_text(encoding="utf-8")
)
self.assertEqual(
[], list(self.root.glob(f".{self.output.name}.*.tmp"))
)
def test_missing_parameter_metadata_and_return_type_create_a_draft(self):
source = self.write_tsf(
"Draft",
"""
function Draft(value);
begin
/// 尚待完善的函数。
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual(
[{"name": "value", "type": "", "desc": ""}],
function["params"],
)
self.assertEqual("", function["returns"])
def test_legacy_document_block_creates_structural_draft(self):
source = self.write_tsf(
"LegacyDraft",
"""
function LegacyDraft(required: integer; optional = 1): array;
begin
{**
@param(required)(integer) 必填值
@param(optional)(integer) 可选值
@return(array) 结果
**}
return array();
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
{
"kind": "function",
"name": "LegacyDraft",
"signature": "LegacyDraft(required, optional)",
"desc": "",
"params": [
{"name": "required", "type": "integer", "desc": ""},
{
"name": "optional",
"type": "",
"optional": True,
"desc": "",
},
],
"returns": "array",
},
self.read_declaration(),
)
def test_json_draft_completes_fixed_fields_for_every_declaration_kind(self):
function_source = self.write_tsf(
"DraftFunction",
"""
function DraftFunction();
begin
/// 草稿函数
return;
end;
""",
)
class_source = self.write_tsf(
"DraftClass",
"""
type DraftClass = class
class function Build(); // 构建草稿
property Title read title_;
value_;
const Limit = 1;
end;
""",
)
unit_source = self.write_tsf(
"DraftUnit",
"""
unit DraftUnit;
interface
function Open();
var Current;
const Limit = 1;
type Item = class
function Read();
end;
implementation
end.
""",
)
result = self.run_cli(function_source, class_source, unit_source)
self.assertEqual(0, result.returncode, result.stderr)
function, cls, unit = self.read_declarations()
self.assertEqual([], function.get("params"))
self.assertEqual("", function.get("returns"))
method, prop, field, constant = cls["members"]
self.assertEqual([], method.get("params"))
self.assertEqual("", method.get("returns"))
self.assertEqual("", prop.get("type"))
self.assertEqual([], prop.get("params"))
self.assertEqual("", field.get("type"))
self.assertEqual("", constant.get("type"))
unit_function, variable, unit_constant, nested_class = unit["members"]
self.assertEqual([], unit_function.get("params"))
self.assertEqual("", unit_function.get("returns"))
self.assertEqual("", variable.get("type"))
self.assertEqual("", unit_constant.get("type"))
self.assertEqual([], nested_class["members"][0].get("params"))
self.assertEqual("", nested_class["members"][0].get("returns"))
for item in (function, cls, unit, method, prop, field, constant):
self.assertNotIn("tags", item)
def test_documented_untyped_parameter_and_return_remain_empty_draft(self):
source = self.write_tsf(
"DocumentedDraft",
"""
function DocumentedDraft(value);
begin
/// 保留没有类型注解的函数
/// @param: value 输入值
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual(
[{"name": "value", "type": "", "desc": "输入值"}],
function["params"],
)
self.assertEqual("", function["returns"])
def test_yaml_draft_uses_the_same_complete_fixed_fields(self):
try:
import yaml
except ImportError:
self.skipTest("未安装 pyyaml")
source = self.write_tsf(
"YamlDraft",
"""
type YamlDraft = class
class function Build(); // 构建草稿
property Title read title_;
const Limit = 1;
end;
""",
)
output = self.root / "draft.yaml"
result = self.run_cli(source, fmt="yaml", output=output)
self.assertEqual(0, result.returncode, result.stderr)
method, prop, constant = yaml.safe_load(
output.read_text(encoding="utf-8")
)["declarations"][0]["members"]
self.assertEqual([], method.get("params"))
self.assertEqual("", method.get("returns"))
self.assertEqual("", prop.get("type"))
self.assertEqual([], prop.get("params"))
self.assertEqual("", constant.get("type"))
def test_help_is_written_in_chinese(self):
result = subprocess.run(
[sys.executable, str(SCRIPT), "--help"],
capture_output=True,
text=True,
encoding="utf-8",
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("用法:", result.stdout)
self.assertIn("位置参数:", result.stdout)
self.assertIn("选项:", result.stdout)
self.assertIn("输出格式", result.stdout)
self.assertIn("function、class 和 unit", result.stdout)
self.assertIn("declarations", result.stdout)
self.assertNotIn("usage:", result.stdout)
self.assertNotIn("positional arguments:", result.stdout)
self.assertNotIn("options:", result.stdout)
def test_return_type_mismatch_creates_structural_draft(self):
source = self.write_tsf(
"Mismatch",
"""
function Mismatch(): integer;
begin
/// 返回一个值。
/// @returns: string
return 1;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("", function["desc"])
self.assertEqual("integer", function["returns"])
def test_late_document_block_creates_structural_draft(self):
source = self.write_tsf(
"LateDoc",
"""
function LateDoc(): integer;
begin
value := 1;
/// 这个文档块出现得太晚。
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("", function["desc"])
self.assertEqual("integer", function["returns"])
def test_missing_body_does_not_borrow_later_function_document(self):
source = self.write_tsf(
"MissingOwnBody",
"function MissingOwnBody(): integer;\n"
"function Helper(): integer;\n"
"begin\n"
" /// 辅助函数文档。\n"
" return 1;\n"
"end;\n",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("MissingOwnBody", function["name"])
self.assertEqual("", function["desc"])
def test_duplicate_enum_value_creates_structural_draft(self):
source = self.write_tsf(
"DuplicateValue",
"""
function DuplicateValue(mode: integer): integer;
begin
/// 检查模式。
/// @param: mode 模式
/// @values: mode
/// 1: 第一项
/// 1: 重复项
return mode;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("", function["desc"])
self.assertEqual(
[{"name": "mode", "type": "integer", "desc": ""}],
function["params"],
)
self.assertEqual("integer", function["returns"])
def test_directives_after_examples_create_structural_draft(self):
source = self.write_tsf(
"BadOrder",
"""
function BadOrder(value: integer): integer;
begin
/// 返回输入值。
/// @example: 基本用法
/// return BadOrder(1);
/// @param: value 输入值
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
function = self.read_declaration()
self.assertEqual("", function["desc"])
self.assertEqual(
[{"name": "value", "type": "integer", "desc": ""}],
function["params"],
)
self.assertEqual("integer", function["returns"])
def test_class_converts_only_first_matching_public_class(self):
source = self.write_tsf(
"Widget",
"""
type Widget = class(BaseOne, BaseTwo)
/// 对外组件。
/// @tags: 组件 示例
public
/// 创建组件。
/// @param: name 组件名称
function create(name: string); overload;
/// 从配置创建。
/// @param: path 配置路径
class function FromConfig(path: string): Widget;
/// 组件标题。
property Title: string read title_ write title_;
protected
/// 下一个编号。
static next_id_: integer;
private
/// 内部状态。
hidden_: string;
end;
type WidgetHelper = class
public
function Hidden(): integer;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
{
"kind": "class",
"name": "Widget",
"desc": "对外组件。",
"tags": ["组件", "示例"],
"bases": ["BaseOne", "BaseTwo"],
"members": [
{
"kind": "method",
"name": "create",
"visibility": "public",
"binding": "instance",
"signature": "create(name)",
"desc": "创建组件。",
"params": [
{
"name": "name",
"type": "string",
"desc": "组件名称",
}
],
"returns": "",
"modifiers": ["overload"],
},
{
"kind": "method",
"name": "FromConfig",
"visibility": "public",
"binding": "class",
"signature": "FromConfig(path)",
"desc": "从配置创建。",
"params": [
{
"name": "path",
"type": "string",
"desc": "配置路径",
}
],
"returns": "Widget",
},
{
"kind": "property",
"name": "Title",
"visibility": "public",
"desc": "组件标题。",
"type": "string",
"params": [],
"access": "readwrite",
},
{
"kind": "field",
"name": "next_id_",
"visibility": "protected",
"desc": "下一个编号。",
"type": "integer",
"static": True,
},
],
},
self.read_declaration(),
)
def test_class_out_of_class_implementation_does_not_duplicate_member(self):
source = self.write_tsf(
"Widget",
"""
type Widget = class
/// 对外组件。
public
/// 保存组件。
/// @param: path 保存路径
function Save(path: string): boolean;
end;
function Widget.Save(path: string): boolean;
begin
return true;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(["Save"], [member["name"] for member in members])
def test_class_method_trailing_comment_fills_missing_description(self):
source = self.write_tsf(
"InlineComments",
"""
type InlineComments = class
class function Plain(value); // 普通行尾说明
class function Overloaded(value); overload; // 重载行尾说明
/// 正式文档说明
class function Documented(value); // 不覆盖正式文档
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["普通行尾说明", "重载行尾说明", "正式文档说明"],
[member["desc"] for member in self.read_declaration()["members"]],
)
def test_class_parameterized_property_preserves_params_and_values(self):
source = self.write_tsf(
"Indexed",
"""
type Indexed = class
/// 索引集合。
public
/// 按索引读取项目。
/// @param: index 项目索引
/// @values: index
/// 0: 第一项
property Items(index: integer): string read GetItem write SetItem;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
member = self.read_declaration()["members"][0]
self.assertEqual("property", member["kind"])
self.assertEqual("string", member["type"])
self.assertEqual("readwrite", member["access"])
self.assertEqual(
[
{
"name": "index",
"type": "integer",
"desc": "项目索引",
"values": [{"value": 0, "desc": "第一项"}],
}
],
member["params"],
)
def test_class_instance_and_static_constants_are_preserved(self):
source = self.write_tsf(
"Constants",
"""
type Constants = class
/// 常量集合。
public
/// 默认数量。
const DefaultSize: integer = 0;
/// 是否禁用。
static const Disabled = false;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(["DefaultSize", "Disabled"], [item["name"] for item in members])
self.assertEqual("integer", members[0]["type"])
self.assertEqual("0", members[0]["value"])
self.assertNotIn("static", members[0])
self.assertEqual("false", members[1]["value"])
self.assertIs(members[1]["static"], True)
def test_class_virtual_and_override_modifiers_preserve_source_order(self):
source = self.write_tsf(
"Modifiers",
"""
type Modifiers = class
/// 修饰符示例。
public
/// 检查状态。
function Inspect(): integer; virtual; overload;
/// 重置状态。
function Reset(); override;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(["virtual", "overload"], members[0]["modifiers"])
self.assertEqual(["override"], members[1]["modifiers"])
def test_class_inline_method_is_collected_once(self):
source = self.write_tsf(
"InlineClass",
"""
type InlineClass = class
/// 内联类。
public
/// 读取当前值。
function Value(): integer;
begin
return 1;
end;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(["Value"], [member["name"] for member in members])
def test_class_skips_unsupported_public_declaration(self):
source = self.write_tsf(
"UnsupportedClass",
"""
type UnsupportedClass = class
public
type Alias = integer;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual([], self.read_declaration()["members"])
def test_missing_class_and_unit_member_types_remain_explicit_drafts(self):
cls_source = self.write_tsf(
"TypeDraft",
"""
type TypeDraft = class
/// 类型草稿。
public
/// 未知字段类型。
value_;
/// 未知属性类型。
property Value read value_;
end;
""",
)
class_result = self.run_cli(cls_source)
self.assertEqual(0, class_result.returncode, class_result.stderr)
class_members = self.read_declaration()["members"]
self.assertEqual("", class_members[0]["type"])
self.assertEqual("", class_members[1]["type"])
self.assertEqual([], class_members[1]["params"])
unit_source = self.write_tsf(
"UnitTypeDraft",
"""
unit UnitTypeDraft;
/// 类型草稿接口。
interface
/// 未知变量类型。
var Current;
implementation
end.
""",
)
unit_result = self.run_cli(unit_source)
self.assertEqual(0, unit_result.returncode, unit_result.stderr)
unit_member = self.read_declaration()["members"][0]
self.assertEqual("", unit_member["type"])
def test_unit_collects_only_interface_members_in_source_order(self):
source = self.write_tsf(
"DemoUnit",
"""
unit DemoUnit;
/// 运行时接口。
/// @tags: 运行时 文档
interface
uses RuntimeSupport;
/// 默认数量。
const DefaultSize = 100;
/// 当前名称。
var CurrentName: string;
/// 打开对象。
/// @param: path 文件路径
function Open(path: string): Document;
type Document = class
/// 文档对象。
public
/// 保存文档。
function Save(): boolean;
end;
implementation
/// 这个实现文档不属于 API。
var hidden_: integer;
type HiddenDocument = class
public
function Hidden(): integer;
end;
const HiddenValue = 1;
function Open(path: string): Document;
begin
return nil;
end;
initialization
/// 生命周期文档也不属于 API。
hidden_ := 1;
finalization
hidden_ := 0;
end.
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
unit = self.read_declaration()
self.assertEqual("unit", unit["kind"])
self.assertEqual("DemoUnit", unit["name"])
self.assertEqual("运行时接口。", unit["desc"])
self.assertEqual(["运行时", "文档"], unit["tags"])
self.assertEqual(
["DefaultSize", "CurrentName", "Open", "Document"],
[member["name"] for member in unit["members"]],
)
self.assertEqual(
{
"kind": "constant",
"name": "DefaultSize",
"desc": "默认数量。",
"type": "",
"value": "100",
},
unit["members"][0],
)
self.assertEqual(
{
"kind": "variable",
"name": "CurrentName",
"desc": "当前名称。",
"type": "string",
},
unit["members"][1],
)
self.assertEqual("Document", unit["members"][2]["returns"])
self.assertEqual("class", unit["members"][3]["kind"])
self.assertEqual("Save", unit["members"][3]["members"][0]["name"])
serialized = json.dumps(unit, ensure_ascii=False)
self.assertNotIn("hidden_", serialized)
self.assertNotIn("HiddenDocument", serialized)
self.assertNotIn("HiddenValue", serialized)
self.assertNotIn("生命周期文档", serialized)
def test_incomplete_interface_class_does_not_scan_implementation(self):
source = self.write_tsf(
"IncompleteInterfaceClass",
"unit IncompleteInterfaceClass;\n"
"interface\n"
"type PublicType = class\n"
"public\n"
"function Keep(): integer;\n"
"implementation\n"
"function Hidden(): integer;\n"
"begin\n"
" return 1;\n"
"end;\n"
"end.\n",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
unit = self.read_declaration()
self.assertEqual(["PublicType"], [member["name"] for member in unit["members"]])
self.assertEqual(
["Keep"],
[member["name"] for member in unit["members"][0]["members"]],
)
def test_unit_filename_mismatch_keeps_declared_name(self):
source = self.write_tsf(
"Wrong",
"unit Right;\ninterface\nimplementation\nend.\n",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("Right", self.read_declaration()["name"])
def test_unit_preserves_multiple_interface_classes_in_source_order(self):
source = self.write_tsf(
"ClassUnit",
"""
unit ClassUnit;
/// 类接口。
interface
type First = class
/// 第一个类。
end;
type Second = class
/// 第二个类。
end;
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(["First", "Second"], [member["name"] for member in members])
def test_unit_unbound_document_is_ignored(self):
source = self.write_tsf(
"UnboundUnit",
"""
unit UnboundUnit;
interface
/// 无法绑定的接口文档。
// 普通注释切断绑定。
var Current: integer;
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
current = self.read_declaration()["members"][0]
self.assertEqual("Current", current["name"])
self.assertEqual("", current["desc"])
def test_unit_unbound_header_document_is_ignored(self):
source = self.write_tsf(
"UnboundHeaderUnit",
"""
unit UnboundHeaderUnit;
/// 无法绑定的 unit 文档。
// 普通注释切断绑定。
interface
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", self.read_declaration()["desc"])
def test_unit_ignores_tokens_after_terminal_end_dot(self):
source = self.write_tsf(
"TrailingUnit",
"""
unit TrailingUnit;
interface
implementation
end.
function Trailing(): integer;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("TrailingUnit", self.read_declaration()["name"])
def test_class_missing_docs_default_visibility_and_private_filter_create_draft(self):
source = self.write_tsf(
"DraftClass",
"""
type DraftClass = class
value_: integer;
private
/// 不应进入草稿。
hidden_: string;
protected
/// 检查状态。
function Inspect();
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
cls = self.read_declaration()
self.assertEqual("", cls["desc"])
self.assertEqual(["value_", "Inspect"], [item["name"] for item in cls["members"]])
self.assertEqual("public", cls["members"][0]["visibility"])
self.assertEqual("", cls["members"][0]["desc"])
self.assertEqual("protected", cls["members"][1]["visibility"])
def test_class_private_multi_name_fields_and_constants_are_ignored(self):
source = self.write_tsf(
"PrivateMulti",
"""
type PrivateMulti = class
/// 私有声明示例。
private
/// 多名称私有字段。
left_, right_: integer;
/// 多名称私有常量。
const First, Second = 1;
public
/// 对外值。
value_: integer;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(["value_"], [member["name"] for member in members])
def test_class_structure_errors_create_best_effort_drafts(self):
cases = {
"WrongName": (
"type Actual = class\nend;\n",
"Actual",
[],
),
"StaticMethod": (
"type StaticMethod = class\npublic\nstatic function Bad();\nend;\n",
"StaticMethod",
["Bad"],
),
"ManyFields": (
"type ManyFields = class\npublic\nleft_, right_: integer;\nend;\n",
"ManyFields",
["left_", "right_"],
),
}
for name, (text, expected_name, expected_members) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual(expected_name, declaration["name"])
self.assertEqual(
expected_members,
[member["name"] for member in declaration["members"]],
)
def test_class_and_unit_constants_split_multiple_names_into_drafts(self):
cases = {
"ManyConstants": (
"type ManyConstants = class\npublic\nconst Left, Right = 1;\nend;\n",
3,
),
"ManyUnitConstants": (
"unit ManyUnitConstants;\n"
"interface\n"
"const Left, Right = 1;\n"
"implementation\n"
"end.\n",
3,
),
}
for name, (text, _line) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["Left", "Right"],
[member["name"] for member in self.read_declaration()["members"]],
)
def test_class_converts_procedure_to_method_draft(self):
source = self.write_tsf(
"ProcedureClass",
"""
type ProcedureClass = class
public
procedure Open();
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
method = self.read_declaration()["members"][0]
self.assertEqual("Open", method["name"])
self.assertEqual("method", method["kind"])
self.assertEqual("", method["returns"])
def test_class_unbound_document_is_ignored(self):
source = self.write_tsf(
"UnboundDoc",
"""
type UnboundDoc = class
/// 类描述。
public
/// 无法绑定的成员描述。
// 普通注释切断绑定。
value_: integer;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
field = self.read_declaration()["members"][0]
self.assertEqual("value_", field["name"])
self.assertEqual("", field["desc"])
def test_unit_converts_shorthand_and_skips_unsupported_types(self):
cases = {
"ShortUnit": (
"unit ShortUnit;\nfunction Open(): integer;\nbegin return 1; end;\nend.\n",
["Open"],
),
"ProcedureUnit": (
"unit ProcedureUnit;\ninterface\nprocedure Open();\nimplementation\nend.\n",
["Open"],
),
"AliasUnit": (
"unit AliasUnit;\ninterface\ntype Size = integer;\nimplementation\nend.\n",
[],
),
}
for name, (text, expected_members) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
expected_members,
[member["name"] for member in self.read_declaration()["members"]],
)
def test_malformed_class_and_unit_documents_create_structural_drafts(self):
cls = self.write_tsf(
"BadClassDocs",
"""
type BadClassDocs = class
public
/// 打开值
/// @param: value {string} 输入值
function Open(value: integer): integer;
end;
""",
)
unit = self.write_tsf(
"BadUnitDocs",
"""
unit BadUnitDocs;
interface
/// 打开值
/// @returns: string
function Open(): integer;
implementation
end.
""",
)
result = self.run_cli(cls, unit)
self.assertEqual(0, result.returncode, result.stderr)
class_method = self.read_declarations()[0]["members"][0]
unit_function = self.read_declarations()[1]["members"][0]
self.assertEqual("", class_method["desc"])
self.assertEqual("integer", class_method["params"][0]["type"])
self.assertEqual("", unit_function["desc"])
self.assertEqual("integer", unit_function["returns"])
def test_incomplete_class_and_unit_keep_recognizable_members(self):
cases = {
"IncompleteClass": (
"type IncompleteClass = class\n"
"public\n"
"function Keep(value: integer): integer;\n",
"Keep",
),
"IncompleteUnit": (
"unit IncompleteUnit;\n"
"function Keep(value: integer): integer;\n",
"Keep",
),
}
for name, (text, expected_member) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
[expected_member],
[member["name"] for member in self.read_declaration()["members"]],
)
def test_broken_member_does_not_hide_following_function(self):
cases = {
"RecoverClass": (
"type RecoverClass = class\n"
"public\n"
"property Broken\n"
"function Keep(): integer;\n"
"end;\n",
"class",
),
"RecoverUnit": (
"unit RecoverUnit;\n"
"interface\n"
"const Broken\n"
"function Keep(): integer;\n"
"implementation\n"
"end.\n",
"unit",
),
}
for name, (text, expected_kind) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual(expected_kind, declaration["kind"])
self.assertIn(
"Keep",
[member["name"] for member in declaration["members"]],
)
def test_unnamed_class_and_unit_functions_are_skipped(self):
cases = {
"UnnamedClassFunction": (
"type UnnamedClassFunction = class\n"
"public\n"
"function\n"
"function Keep(): integer;\n"
"end;\n",
"class",
),
"UnnamedUnitFunction": (
"unit UnnamedUnitFunction;\n"
"interface\n"
"function\n"
"function Keep(): integer;\n"
"implementation\n"
"end.\n",
"unit",
),
}
for name, (text, expected_kind) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual(expected_kind, declaration["kind"])
self.assertEqual(
["Keep"],
[member["name"] for member in declaration["members"]],
)
def test_unnamed_members_do_not_hide_following_functions(self):
cases = {
"UnnamedProperty": (
"type UnnamedProperty = class\n"
"public\n"
"property\n"
"function Keep(): integer;\n"
"end;\n",
"class",
),
"UnnamedConstant": (
"unit UnnamedConstant;\n"
"interface\n"
"const\n"
"function Keep(): integer;\n"
"implementation\n"
"end.\n",
"unit",
),
}
for name, (text, expected_kind) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual(expected_kind, declaration["kind"])
self.assertEqual(
["Keep"],
[member["name"] for member in declaration["members"]],
)
def test_method_missing_signature_semicolon_does_not_hide_next_member(self):
source = self.write_tsf(
"BrokenInlineMethod",
"type BrokenInlineMethod = class\n"
"public\n"
"function Broken()\n"
"begin\n"
" return 1;\n"
"end;\n"
"function Keep(): integer;\n"
"end;\n",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["Broken", "Keep"],
[member["name"] for member in self.read_declaration()["members"]],
)
def test_broken_headers_do_not_hide_first_function(self):
cases = {
"BrokenBase": (
"type BrokenBase = class(Base\n"
"function Keep(): integer;\n",
"class",
),
"MissingClassName": (
"type\n"
"class\n"
"function Keep(): integer;\n",
"class",
),
"BrokenUnitHeader": (
"unit BrokenUnitHeader\n"
"function Keep(): integer;\n",
"unit",
),
"MissingUnitName": (
"unit\n"
"function Keep(): integer;\n",
"unit",
),
}
for name, (text, expected_kind) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual(expected_kind, declaration["kind"])
self.assertEqual(
["Keep"],
[member["name"] for member in declaration["members"]],
)
def test_declaration_truncation_after_name_always_creates_a_draft(self):
module = load_script()
samples = {
"function": (
"TruncateFunction.tsf",
"function TruncateFunction(required: integer; optional = 1): array;\n"
"begin\n"
" return array();\n"
"end;\n",
),
"class": (
"TruncateClass.tsf",
"type TruncateClass = class(Base)\n"
"public\n"
"function Open(value: integer): string;\n"
"property Value: integer read value_ write value_;\n"
"const Limit = 2;\n"
"value_: integer;\n"
"end;\n",
),
"unit": (
"TruncateUnit.tsf",
"unit TruncateUnit;\n"
"interface\n"
"function Open(value: integer): string;\n"
"const Limit = 2;\n"
"var Current: integer;\n"
"implementation\n"
"end.\n",
),
}
for expected_kind, (filename, source) in samples.items():
name_end = module.tokenize(source)[1].end
for cut in range(name_end, len(source) + 1):
with self.subTest(kind=expected_kind, cut=cut):
declaration = module.complete_draft_fields(
module.convert_source(source[:cut], Path(filename))
)
self.assertEqual(expected_kind, declaration["kind"])
self.assertTrue(declaration["name"])
def test_unit_fallback_keeps_bare_const_and_var_sections(self):
source = self.write_tsf(
"RecoverSections",
"""
unit RecoverSections;
interface
/// 无法绑定
// 切断文档绑定
const First = 1;
Second = 2;
var Current: integer;
Previous: integer;
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["First", "Second", "Current", "Previous"],
[member["name"] for member in self.read_declaration()["members"]],
)
def test_unit_var_and_const_sections_keep_each_declaration(self):
source = self.write_tsf(
"StateUnit",
"""
unit StateUnit;
/// 状态接口。
interface
var
/// 当前名称。
CurrentName: string;
/// 当前编号。
CurrentId: integer;
const
/// 默认名称。
DefaultName = "demo";
/// 默认编号。
DefaultId = 1;
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
members = self.read_declaration()["members"]
self.assertEqual(
["CurrentName", "CurrentId", "DefaultName", "DefaultId"],
[member["name"] for member in members],
)
self.assertEqual(
["当前名称。", "当前编号。", "默认名称。", "默认编号。"],
[member["desc"] for member in members],
)
def test_function_overloads_are_preserved_in_input_order(self):
first = self.write_nested_tsf(
"first",
"Parse",
"""
function Parse(path: string): integer;
begin
/// 按路径解析。
/// @param: path 文件路径
return 1;
end;
""",
)
second = self.write_nested_tsf(
"second",
"Parse",
"""
function Parse(mode: integer): integer;
begin
/// 按模式解析。
/// @param: mode 解析模式
return 1;
end;
""",
)
result = self.run_cli(first, second)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["Parse(path)", "Parse(mode)"],
[item["signature"] for item in self.read_declarations()],
)
def test_duplicate_function_signature_is_preserved_in_input_order(self):
first = self.write_nested_tsf(
"first",
"Parse",
"""
function Parse(value: string): integer;
begin
/// 解析值。
/// @param: value 输入值
return 1;
end;
""",
)
second = self.write_nested_tsf(
"second",
"parse",
"""
function parse(value: string): integer;
begin
/// 再次解析值。
/// @param: value 输入值
return 1;
end;
""",
)
result = self.run_cli(first, second)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["Parse(value)", "parse(value)"],
[item["signature"] for item in self.read_declarations()],
)
def test_duplicate_class_and_unit_names_are_preserved_in_input_order(self):
cases = (
(
self.write_nested_tsf(
"class-a", "Widget", "type Widget = class\nend;\n"
),
self.write_nested_tsf(
"class-b", "widget", "type widget = class\nend;\n"
),
"重复 class",
),
(
self.write_nested_tsf(
"unit-a",
"Runtime",
"unit Runtime;\ninterface\nimplementation\nend.\n",
),
self.write_nested_tsf(
"unit-b",
"runtime",
"unit runtime;\ninterface\nimplementation\nend.\n",
),
"重复 unit",
),
)
for first, second, kind in cases:
with self.subTest(kind=kind):
result = self.run_cli(first, second)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
2,
len(self.read_declarations()),
)
def test_same_name_across_kinds_is_allowed(self):
function = self.write_nested_tsf(
"function",
"Widget",
"""
function Widget(): integer;
begin
/// 同名函数。
return 1;
end;
""",
)
cls = self.write_nested_tsf(
"class", "Widget", "type Widget = class\nend;\n"
)
result = self.run_cli(function, cls)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["function", "class"],
[item["kind"] for item in self.read_declarations()],
)
def test_multiple_classes_and_units_are_allowed(self):
inputs = (
self.write_tsf("FirstClass", "type FirstClass = class\nend;\n"),
self.write_tsf("SecondClass", "type SecondClass = class\nend;\n"),
self.write_tsf(
"FirstUnit",
"unit FirstUnit;\ninterface\nimplementation\nend.\n",
),
self.write_tsf(
"SecondUnit",
"unit SecondUnit;\ninterface\nimplementation\nend.\n",
),
)
result = self.run_cli(*inputs)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
["FirstClass", "SecondClass", "FirstUnit", "SecondUnit"],
[item["name"] for item in self.read_declarations()],
)
def test_top_level_procedure_creates_function_draft(self):
source = self.write_tsf(
"ProcedureDemo",
"procedure ProcedureDemo(value: integer);\nbegin\nend;\n",
)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(
{
"kind": "function",
"name": "ProcedureDemo",
"signature": "ProcedureDemo(value)",
"desc": "",
"params": [
{"name": "value", "type": "integer", "desc": ""}
],
"returns": "",
},
self.read_declaration(),
)
def test_malformed_top_level_function_keeps_recognizable_signature(self):
cases = {
"MissingBody": (
"function MissingBody(value: integer): array;\n",
"MissingBody",
[{"name": "value", "type": "integer", "desc": ""}],
"array",
),
"BrokenParams": (
"function BrokenParams(required: integer; optional = 1\n",
"BrokenParams",
[
{"name": "required", "type": "integer", "desc": ""},
{
"name": "optional",
"type": "",
"optional": True,
"desc": "",
},
],
"",
),
"MissingName": (
"function\n",
"MissingName",
[],
"",
),
}
for filename, (text, name, params, returns) in cases.items():
with self.subTest(filename=filename):
source = self.write_tsf(filename, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual(name, declaration["name"])
parameter_names = ", ".join(
parameter["name"] for parameter in params
)
self.assertEqual(
f"{name}({parameter_names})",
declaration["signature"],
)
self.assertEqual(params, declaration["params"])
self.assertEqual(returns, declaration["returns"])
def test_unrecognized_tsf_header_still_creates_function_draft(self):
cases = {
"LooseCall": (
"LegacyName(first, second)\n",
"LegacyName",
["first", "second"],
),
"EmptySource": ("", "EmptySource", []),
}
for filename, (text, name, params) in cases.items():
with self.subTest(filename=filename):
source = self.write_tsf(filename, text)
result = self.run_cli(source)
self.assertEqual(0, result.returncode, result.stderr)
declaration = self.read_declaration()
self.assertEqual("function", declaration["kind"])
self.assertEqual(name, declaration["name"])
self.assertEqual(
params,
[parameter["name"] for parameter in declaration["params"]],
)
if __name__ == "__main__":
unittest.main()