Files
playbook/tools/tsl-codegen/tests/test_convert_tsf.py
T
2026-07-29 07:58:05 +00:00

1591 lines
52 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_reports_param_line_and_preserves_output(self):
source = self.write_tsf(
"ParamTypeMismatch",
"""
function ParamTypeMismatch(value: integer): integer;
begin
/// 校验参数类型
/// @param: value {string} 输入值
return value;
end;
""",
)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:4:", result.stderr)
self.assertIn("参数 value 的类型与函数声明不一致", result.stderr)
self.assertIn("注释为 string,声明为 integer", result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
def test_parameter_type_braces_must_be_complete_and_nonempty(self):
cases = {
"EmptyParamType": (
"/// @param: value {} 输入值",
"@param: 参数类型不能为空",
),
"UnclosedParamType": (
"/// @param: value {integer 输入值",
"@param: 参数类型缺少右花括号",
),
"MissingParamDescription": (
"/// @param: value {integer}",
"@param: 参数说明不能为空",
),
}
for name, (param_line, expected) 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(1, result.returncode)
self.assertIn(f"{source}:4:", result.stderr)
self.assertIn(expected, result.stderr)
self.assertFalse(self.output.exists())
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_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_reports_directive_line_and_preserves_output(self):
source = self.write_tsf(
"Mismatch",
"""
function Mismatch(): integer;
begin
/// 返回一个值。
/// @returns: string
return 1;
end;
""",
)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:4:", result.stderr)
self.assertIn("返回类型与函数声明不一致", result.stderr)
self.assertIn("注释为 string,声明为 integer", result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
def test_document_block_must_be_first_content_after_begin(self):
source = self.write_tsf(
"LateDoc",
"""
function LateDoc(): integer;
begin
value := 1;
/// 这个文档块出现得太晚。
return value;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:3:", result.stderr)
self.assertIn("begin 后第一段内容必须是 /// 文档块", result.stderr)
self.assertFalse(self.output.exists())
def test_duplicate_enum_value_is_rejected(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(1, result.returncode)
self.assertIn(f"{source}:7:", result.stderr)
self.assertIn("枚举值重复", result.stderr)
def test_directives_after_examples_are_rejected(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(1, result.returncode)
self.assertIn(f"{source}:6:", result.stderr)
self.assertIn("示例组之后不能再写函数级指令", result.stderr)
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_rejects_unsupported_public_declaration_at_exact_line(self):
source = self.write_tsf(
"UnsupportedClass",
"""
type UnsupportedClass = class
public
type Alias = integer;
end;
""",
)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:3:", result.stderr)
self.assertIn("class 不支持成员声明:type", result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
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_unit_filename_must_match_name_at_declaration_line(self):
source = self.write_tsf(
"Wrong",
"unit Right;\ninterface\nimplementation\nend.\n",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:1:", result.stderr)
self.assertIn("unit 名称必须与文件名一致", result.stderr)
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_reports_exact_line(self):
source = self.write_tsf(
"UnboundUnit",
"""
unit UnboundUnit;
interface
/// 无法绑定的接口文档。
// 普通注释切断绑定。
var Current: integer;
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:3:", result.stderr)
self.assertIn("unit interface 文档块无法绑定", result.stderr)
def test_unit_unbound_header_document_reports_exact_line(self):
source = self.write_tsf(
"UnboundHeaderUnit",
"""
unit UnboundHeaderUnit;
/// 无法绑定的 unit 文档。
// 普通注释切断绑定。
interface
implementation
end.
""",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:2:", result.stderr)
self.assertIn("unit interface 文档块无法绑定", result.stderr)
def test_unit_end_dot_must_be_the_terminal_tokens(self):
source = self.write_tsf(
"TrailingUnit",
"""
unit TrailingUnit;
interface
implementation
end.
function Trailing(): integer;
""",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:5:", result.stderr)
self.assertIn("end. 必须结束整个文件", result.stderr)
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_preserve_existing_output(self):
cases = {
"WrongName": (
"type Actual = class\nend;\n",
1,
"对外 class 名称必须与文件名一致",
),
"StaticMethod": (
"type StaticMethod = class\npublic\nstatic function Bad();\nend;\n",
3,
"不存在 static function",
),
"ManyFields": (
"type ManyFields = class\npublic\nleft_, right_: integer;\nend;\n",
3,
"一项一条声明",
),
}
for name, (text, line, expected) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:{line}:", result.stderr)
self.assertIn(expected, result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
def test_class_and_unit_constants_reject_multiple_names_at_declaration_line(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)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:{line}:", result.stderr)
self.assertIn("对外常量必须一项一条声明", result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
def test_class_rejects_procedure_at_declaration_line(self):
source = self.write_tsf(
"ProcedureClass",
"""
type ProcedureClass = class
public
procedure Open();
end;
""",
)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:3:", result.stderr)
self.assertIn("class 暂不支持 procedure", result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
def test_class_unbound_document_reports_its_line(self):
source = self.write_tsf(
"UnboundDoc",
"""
type UnboundDoc = class
/// 类描述。
public
/// 无法绑定的成员描述。
// 普通注释切断绑定。
value_: integer;
end;
""",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:4:", result.stderr)
self.assertIn("文档块无法绑定", result.stderr)
def test_unit_rejects_shorthand_procedure_and_non_class_type(self):
cases = {
"ShortUnit": (
"unit ShortUnit;\nfunction Open(): integer;\nbegin return 1; end;\nend.\n",
1,
"显式 interface",
),
"ProcedureUnit": (
"unit ProcedureUnit;\ninterface\nprocedure Open();\nimplementation\nend.\n",
3,
"暂不支持 procedure",
),
"AliasUnit": (
"unit AliasUnit;\ninterface\ntype Size = integer;\nimplementation\nend.\n",
3,
"只支持 class type",
),
}
for name, (text, line, expected) in cases.items():
with self.subTest(name=name):
source = self.write_tsf(name, text)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn(f"{source}:{line}:", result.stderr)
self.assertIn(expected, result.stderr)
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_preserves_existing_output(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;
""",
)
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(first, second)
self.assertEqual(1, result.returncode)
self.assertIn("重复 function signature", result.stderr)
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
def test_duplicate_class_and_unit_names_are_rejected_case_insensitively(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, expected in cases:
with self.subTest(expected=expected):
self.output.write_text("原内容\n", encoding="utf-8")
result = self.run_cli(first, second)
self.assertEqual(1, result.returncode)
self.assertIn(expected, result.stderr)
self.assertEqual(
"原内容\n", self.output.read_text(encoding="utf-8")
)
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_remains_unsupported(self):
source = self.write_tsf(
"ProcedureDemo",
"procedure ProcedureDemo();\nbegin\nend;\n",
)
result = self.run_cli(source)
self.assertEqual(1, result.returncode)
self.assertIn("目前支持独立顶层 function、class 和完整 unit", result.stderr)
if __name__ == "__main__":
unittest.main()