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