Add TSF conversion and shared Markdown recognition for mixed function, class, and unit declarations. Extend generation, linting, indexing, examples, and tests around the unified declaration model.
1591 lines
52 KiB
Python
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()
|