@@ -237,7 +237,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.read_declaration()["params"],
|
||||
)
|
||||
|
||||
def test_parameter_type_mismatch_reports_param_line_and_preserves_output(self):
|
||||
def test_parameter_type_mismatch_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ParamTypeMismatch",
|
||||
"""
|
||||
@@ -249,32 +249,24 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
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"))
|
||||
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_parameter_type_braces_must_be_complete_and_nonempty(self):
|
||||
def test_malformed_parameter_docs_create_structural_draft(self):
|
||||
cases = {
|
||||
"EmptyParamType": (
|
||||
"/// @param: value {} 输入值",
|
||||
"@param: 参数类型不能为空",
|
||||
),
|
||||
"UnclosedParamType": (
|
||||
"/// @param: value {integer 输入值",
|
||||
"@param: 参数类型缺少右花括号",
|
||||
),
|
||||
"MissingParamDescription": (
|
||||
"/// @param: value {integer}",
|
||||
"@param: 参数说明不能为空",
|
||||
),
|
||||
"EmptyParamType": "/// @param: value {} 输入值",
|
||||
"UnclosedParamType": "/// @param: value {integer 输入值",
|
||||
"MissingParamDescription": "/// @param: value {integer}",
|
||||
}
|
||||
for name, (param_line, expected) in cases.items():
|
||||
for name, param_line in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(
|
||||
name,
|
||||
@@ -290,10 +282,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
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())
|
||||
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(
|
||||
@@ -514,6 +510,45 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
)
|
||||
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",
|
||||
@@ -649,7 +684,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.assertNotIn("positional arguments:", result.stdout)
|
||||
self.assertNotIn("options:", result.stdout)
|
||||
|
||||
def test_return_type_mismatch_reports_directive_line_and_preserves_output(self):
|
||||
def test_return_type_mismatch_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"Mismatch",
|
||||
"""
|
||||
@@ -661,17 +696,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
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"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_document_block_must_be_first_content_after_begin(self):
|
||||
def test_late_document_block_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"LateDoc",
|
||||
"""
|
||||
@@ -686,12 +718,30 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
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())
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_duplicate_enum_value_is_rejected(self):
|
||||
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",
|
||||
"""
|
||||
@@ -709,11 +759,16 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:7:", result.stderr)
|
||||
self.assertIn("枚举值重复", result.stderr)
|
||||
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_are_rejected(self):
|
||||
def test_directives_after_examples_create_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"BadOrder",
|
||||
"""
|
||||
@@ -730,9 +785,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:6:", result.stderr)
|
||||
self.assertIn("示例组之后不能再写函数级指令", result.stderr)
|
||||
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(
|
||||
@@ -982,7 +1042,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
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):
|
||||
def test_class_skips_unsupported_public_declaration(self):
|
||||
source = self.write_tsf(
|
||||
"UnsupportedClass",
|
||||
"""
|
||||
@@ -992,14 +1052,10 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
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"))
|
||||
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(
|
||||
@@ -1126,7 +1182,33 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.assertNotIn("HiddenValue", serialized)
|
||||
self.assertNotIn("生命周期文档", serialized)
|
||||
|
||||
def test_unit_filename_must_match_name_at_declaration_line(self):
|
||||
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",
|
||||
@@ -1134,9 +1216,8 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:1:", result.stderr)
|
||||
self.assertIn("unit 名称必须与文件名一致", result.stderr)
|
||||
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(
|
||||
@@ -1162,7 +1243,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["First", "Second"], [member["name"] for member in members])
|
||||
|
||||
def test_unit_unbound_document_reports_exact_line(self):
|
||||
def test_unit_unbound_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundUnit",
|
||||
"""
|
||||
@@ -1178,11 +1259,12 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("unit interface 文档块无法绑定", result.stderr)
|
||||
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_reports_exact_line(self):
|
||||
def test_unit_unbound_header_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundHeaderUnit",
|
||||
"""
|
||||
@@ -1197,11 +1279,10 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:2:", result.stderr)
|
||||
self.assertIn("unit interface 文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("", self.read_declaration()["desc"])
|
||||
|
||||
def test_unit_end_dot_must_be_the_terminal_tokens(self):
|
||||
def test_unit_ignores_tokens_after_terminal_end_dot(self):
|
||||
source = self.write_tsf(
|
||||
"TrailingUnit",
|
||||
"""
|
||||
@@ -1215,9 +1296,8 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:5:", result.stderr)
|
||||
self.assertIn("end. 必须结束整个文件", result.stderr)
|
||||
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(
|
||||
@@ -1269,35 +1349,37 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["value_"], [member["name"] for member in members])
|
||||
|
||||
def test_class_structure_errors_preserve_existing_output(self):
|
||||
def test_class_structure_errors_create_best_effort_drafts(self):
|
||||
cases = {
|
||||
"WrongName": (
|
||||
"type Actual = class\nend;\n",
|
||||
1,
|
||||
"对外 class 名称必须与文件名一致",
|
||||
"Actual",
|
||||
[],
|
||||
),
|
||||
"StaticMethod": (
|
||||
"type StaticMethod = class\npublic\nstatic function Bad();\nend;\n",
|
||||
3,
|
||||
"不存在 static function",
|
||||
"StaticMethod",
|
||||
["Bad"],
|
||||
),
|
||||
"ManyFields": (
|
||||
"type ManyFields = class\npublic\nleft_, right_: integer;\nend;\n",
|
||||
3,
|
||||
"一项一条声明",
|
||||
"ManyFields",
|
||||
["left_", "right_"],
|
||||
),
|
||||
}
|
||||
for name, (text, line, expected) in cases.items():
|
||||
for name, (text, expected_name, expected_members) 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"))
|
||||
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_reject_multiple_names_at_declaration_line(self):
|
||||
def test_class_and_unit_constants_split_multiple_names_into_drafts(self):
|
||||
cases = {
|
||||
"ManyConstants": (
|
||||
"type ManyConstants = class\npublic\nconst Left, Right = 1;\nend;\n",
|
||||
@@ -1312,19 +1394,19 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
3,
|
||||
),
|
||||
}
|
||||
for name, (text, line) in cases.items():
|
||||
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"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Left", "Right"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_class_rejects_procedure_at_declaration_line(self):
|
||||
def test_class_converts_procedure_to_method_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ProcedureClass",
|
||||
"""
|
||||
@@ -1334,16 +1416,15 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
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"))
|
||||
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_reports_its_line(self):
|
||||
def test_class_unbound_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundDoc",
|
||||
"""
|
||||
@@ -1359,35 +1440,323 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("文档块无法绑定", result.stderr)
|
||||
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_rejects_shorthand_procedure_and_non_class_type(self):
|
||||
def test_unit_converts_shorthand_and_skips_unsupported_types(self):
|
||||
cases = {
|
||||
"ShortUnit": (
|
||||
"unit ShortUnit;\nfunction Open(): integer;\nbegin return 1; end;\nend.\n",
|
||||
1,
|
||||
"显式 interface",
|
||||
["Open"],
|
||||
),
|
||||
"ProcedureUnit": (
|
||||
"unit ProcedureUnit;\ninterface\nprocedure Open();\nimplementation\nend.\n",
|
||||
3,
|
||||
"暂不支持 procedure",
|
||||
["Open"],
|
||||
),
|
||||
"AliasUnit": (
|
||||
"unit AliasUnit;\ninterface\ntype Size = integer;\nimplementation\nend.\n",
|
||||
3,
|
||||
"只支持 class type",
|
||||
[],
|
||||
),
|
||||
}
|
||||
for name, (text, line, expected) in cases.items():
|
||||
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(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
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(
|
||||
@@ -1458,7 +1827,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
[item["signature"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_duplicate_function_signature_preserves_existing_output(self):
|
||||
def test_duplicate_function_signature_is_preserved_in_input_order(self):
|
||||
first = self.write_nested_tsf(
|
||||
"first",
|
||||
"Parse",
|
||||
@@ -1483,15 +1852,15 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
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"))
|
||||
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_rejected_case_insensitively(self):
|
||||
def test_duplicate_class_and_unit_names_are_preserved_in_input_order(self):
|
||||
cases = (
|
||||
(
|
||||
self.write_nested_tsf(
|
||||
@@ -1516,16 +1885,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
"重复 unit",
|
||||
),
|
||||
)
|
||||
for first, second, expected in cases:
|
||||
with self.subTest(expected=expected):
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
for first, second, kind in cases:
|
||||
with self.subTest(kind=kind):
|
||||
result = self.run_cli(first, second)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
"原内容\n", self.output.read_text(encoding="utf-8")
|
||||
2,
|
||||
len(self.read_declarations()),
|
||||
)
|
||||
|
||||
def test_same_name_across_kinds_is_allowed(self):
|
||||
@@ -1574,16 +1941,100 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
[item["name"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_top_level_procedure_remains_unsupported(self):
|
||||
def test_top_level_procedure_creates_function_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ProcedureDemo",
|
||||
"procedure ProcedureDemo();\nbegin\nend;\n",
|
||||
"procedure ProcedureDemo(value: integer);\nbegin\nend;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("目前支持独立顶层 function、class 和完整 unit", result.stderr)
|
||||
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__":
|
||||
|
||||
Reference in New Issue
Block a user