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()