@@ -0,0 +1,405 @@
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
TOOL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = TOOL_ROOT.parents[1]
|
||||
CONVERT = TOOL_ROOT / "scripts" / "convert_tsf.py"
|
||||
GENERATE = TOOL_ROOT / "scripts" / "generate.py"
|
||||
LINT = TOOL_ROOT / "scripts" / "lint.py"
|
||||
BUILD_INDEX = TOOL_ROOT / "scripts" / "build_index.py"
|
||||
LOOKUP = REPO_ROOT / "skills" / "tsl-api-reference" / "scripts" / "lookup.py"
|
||||
STANDARD = TOOL_ROOT / "STANDARD.md"
|
||||
README = TOOL_ROOT / "README.md"
|
||||
EXAMPLE_JSON = TOOL_ROOT / "examples" / "example.json"
|
||||
EXAMPLE_YAML = TOOL_ROOT / "examples" / "example.yaml"
|
||||
SKILL = REPO_ROOT / "skills" / "tsl-api-reference" / "SKILL.md"
|
||||
|
||||
|
||||
class UnifiedPipelineTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp_dir.name)
|
||||
self.skill_dir = self.root / "skills" / "tsl-api-reference"
|
||||
|
||||
def tearDown(self):
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_command(self, *args, cwd=None):
|
||||
return subprocess.run(
|
||||
[sys.executable, *map(str, args)],
|
||||
cwd=cwd or REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def run_pipeline(self, sources, queries):
|
||||
tsf_paths = []
|
||||
for name, source in sources:
|
||||
tsf_path = self.root / f"{name}.tsf"
|
||||
tsf_path.write_text(
|
||||
textwrap.dedent(source).lstrip(), encoding="utf-8"
|
||||
)
|
||||
tsf_paths.append(tsf_path)
|
||||
|
||||
page_path = "base/mixed"
|
||||
recordings = {}
|
||||
recording_paths = {}
|
||||
for output_format in ("json", "yaml"):
|
||||
recording = self.root / f"mixed.{output_format}"
|
||||
convert = self.run_command(
|
||||
CONVERT,
|
||||
*tsf_paths,
|
||||
"--format",
|
||||
output_format,
|
||||
"--module",
|
||||
"示例 / 混合 API",
|
||||
"--path",
|
||||
page_path,
|
||||
"--output",
|
||||
recording,
|
||||
)
|
||||
self.assertEqual(0, convert.returncode, convert.stderr)
|
||||
recording_paths[output_format] = recording
|
||||
text = recording.read_text(encoding="utf-8")
|
||||
recordings[output_format] = (
|
||||
json.loads(text)
|
||||
if output_format == "json"
|
||||
else yaml.safe_load(text)
|
||||
)
|
||||
self.assertEqual(recordings["json"], recordings["yaml"])
|
||||
|
||||
markdown = (
|
||||
self.skill_dir
|
||||
/ "references"
|
||||
/ "codegen"
|
||||
/ "project"
|
||||
/ f"{page_path}.md"
|
||||
)
|
||||
generated_markdown = {}
|
||||
for output_format, recording in recording_paths.items():
|
||||
generate = self.run_command(GENERATE, recording, cwd=self.root)
|
||||
self.assertEqual(0, generate.returncode, generate.stderr)
|
||||
self.assertTrue(markdown.is_file())
|
||||
generated_markdown[output_format] = markdown.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
lint = self.run_command(LINT, "--file", markdown, "--strict")
|
||||
self.assertEqual(0, lint.returncode, lint.stdout + lint.stderr)
|
||||
self.assertEqual(
|
||||
generated_markdown["json"], generated_markdown["yaml"]
|
||||
)
|
||||
|
||||
build = self.run_command(BUILD_INDEX, "--skill-dir", self.skill_dir)
|
||||
self.assertEqual(0, build.returncode, build.stderr)
|
||||
check = self.run_command(
|
||||
BUILD_INDEX, "--skill-dir", self.skill_dir, "--check"
|
||||
)
|
||||
self.assertEqual(0, check.returncode, check.stderr)
|
||||
|
||||
lookup_outputs = {}
|
||||
for query in queries:
|
||||
lookup = self.run_command(
|
||||
LOOKUP,
|
||||
"--name",
|
||||
query,
|
||||
"--tsv",
|
||||
self.skill_dir / "data" / "function_index.tsv",
|
||||
)
|
||||
self.assertEqual(0, lookup.returncode, lookup.stderr)
|
||||
lookup_outputs[query] = lookup.stdout
|
||||
return (
|
||||
recordings["json"],
|
||||
generated_markdown["json"],
|
||||
lookup_outputs,
|
||||
)
|
||||
|
||||
@unittest.skipUnless(yaml is not None, "pyyaml is not installed")
|
||||
def test_mixed_pipeline_convert_generate_lint_index_lookup(self):
|
||||
recording, markdown, lookups = self.run_pipeline(
|
||||
sources=[
|
||||
(
|
||||
"Widget",
|
||||
"""
|
||||
type Widget = class
|
||||
/// 组件
|
||||
public
|
||||
/// 重命名组件
|
||||
/// @param: name 组件名称
|
||||
function Rename(name: string): string;
|
||||
/// 创建组件
|
||||
class function Create(): Widget;
|
||||
/// 组件标题
|
||||
property Title: string read title_ write title_;
|
||||
/// 原始标题
|
||||
property RawTitle read title_;
|
||||
/// 标题存储
|
||||
title_: string;
|
||||
/// 组件数量
|
||||
static Count: integer;
|
||||
/// 默认类型
|
||||
const DefaultKind = 1;
|
||||
/// 最大数量
|
||||
static const Maximum = 100;
|
||||
end;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"OpenWidget",
|
||||
"""
|
||||
function OpenWidget(path: string): Widget;
|
||||
begin
|
||||
/// 打开组件
|
||||
/// @param: path 组件路径
|
||||
return nil;
|
||||
end;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"DemoUnit",
|
||||
"""
|
||||
unit DemoUnit;
|
||||
/// 接口
|
||||
interface
|
||||
/// 默认大小
|
||||
const DefaultSize = 100;
|
||||
/// 当前文档
|
||||
var CurrentDocument: Document;
|
||||
/// 打开文档
|
||||
/// @param: path 文档路径
|
||||
function OpenDocument(path: string): Document;
|
||||
type Document = class
|
||||
/// 文档
|
||||
public
|
||||
/// 保存
|
||||
function Save(): boolean;
|
||||
/// 创建文档
|
||||
class function Create(): Document;
|
||||
/// 文档标题
|
||||
property Title: string read title_ write title_;
|
||||
private
|
||||
title_: string;
|
||||
end;
|
||||
implementation
|
||||
end.
|
||||
""",
|
||||
),
|
||||
],
|
||||
queries=("Widget", "OpenWidget", "DemoUnit.Document"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
["class", "function", "unit"],
|
||||
[item["kind"] for item in recording["declarations"]],
|
||||
)
|
||||
widget_members = recording["declarations"][0]["members"]
|
||||
raw_title = next(
|
||||
item for item in widget_members if item["name"] == "RawTitle"
|
||||
)
|
||||
self.assertEqual("", raw_title["type"])
|
||||
self.assertEqual([], raw_title["params"])
|
||||
self.assertIn("## `Widget`\n\n声明:class", markdown)
|
||||
self.assertIn("## `OpenWidget(path)`\n\n声明:function", markdown)
|
||||
self.assertIn("## `DemoUnit`\n\n声明:unit", markdown)
|
||||
self.assertIn(
|
||||
"### `Create()`\n\n声明:class function", lookups["Widget"]
|
||||
)
|
||||
self.assertIn(
|
||||
"### `Rename(name)`\n\n声明:function", lookups["Widget"]
|
||||
)
|
||||
self.assertNotIn("## `OpenWidget()`", lookups["Widget"])
|
||||
self.assertIn("声明:function", lookups["OpenWidget"])
|
||||
self.assertNotIn("## `DemoUnit`", lookups["OpenWidget"])
|
||||
self.assertIn(
|
||||
"#### `Save()`\n\n声明:function", lookups["DemoUnit.Document"]
|
||||
)
|
||||
self.assertIn(
|
||||
"#### `Create()`\n\n声明:class function",
|
||||
lookups["DemoUnit.Document"],
|
||||
)
|
||||
|
||||
def test_standard_and_readme_describe_unified_declarations(self):
|
||||
standard = STANDARD.read_text(encoding="utf-8")
|
||||
readme = README.read_text(encoding="utf-8")
|
||||
skill = SKILL.read_text(encoding="utf-8")
|
||||
|
||||
self.assertTrue(standard.startswith("# TSL API 文档标准\n"))
|
||||
self.assertNotIn("根字段 `functions`、`class`、`unit`", standard)
|
||||
self.assertNotIn("每个 markdown 叶子页只记录一种页面类型", standard)
|
||||
for fragment in (
|
||||
"declarations",
|
||||
"声明:function",
|
||||
"声明:class",
|
||||
"声明:unit",
|
||||
"顶级 function",
|
||||
"顶级 class",
|
||||
"顶级 unit",
|
||||
"综合示例",
|
||||
):
|
||||
self.assertIn(fragment, standard)
|
||||
for heading in (
|
||||
"### function\n",
|
||||
"### class\n",
|
||||
"### unit\n",
|
||||
"### 综合示例\n",
|
||||
):
|
||||
self.assertIn(heading, standard)
|
||||
for heading in (
|
||||
"### 顶级 function\n",
|
||||
"### 顶级 class\n",
|
||||
"### 顶级 unit\n",
|
||||
"### 混合页面示例\n",
|
||||
):
|
||||
self.assertNotIn(heading, standard)
|
||||
for fragment in (
|
||||
"declarations",
|
||||
"混合输入",
|
||||
"OpenXmlAttribute.tsf",
|
||||
"OpenXmlRuntime.tsf",
|
||||
"13 列",
|
||||
"property 类型可选",
|
||||
):
|
||||
self.assertIn(fragment, readme)
|
||||
self.assertIn(
|
||||
"## `OpenXmlAttribute`\n\n声明:class", readme
|
||||
)
|
||||
self.assertIn(
|
||||
"### `CreateVirtual(position, row_index, _story)`\n\n"
|
||||
"声明:class function",
|
||||
readme,
|
||||
)
|
||||
self.assertNotIn("property/field/variable 缺少类型", readme)
|
||||
self.assertIn("混合页面", skill)
|
||||
self.assertIn("page#anchor", skill)
|
||||
|
||||
def test_standard_class_methods_reuse_top_level_function_structure(self):
|
||||
standard = STANDARD.read_text(encoding="utf-8")
|
||||
class_section = standard.split("### class\n", 1)[1].split(
|
||||
"### unit\n", 1
|
||||
)[0]
|
||||
normalized_class = " ".join(class_section.split())
|
||||
normalized_standard = " ".join(standard.split())
|
||||
|
||||
self.assertIn("正文结构与顶级 function 相同", normalized_class)
|
||||
self.assertIn("实例 function 和 class function", normalized_class)
|
||||
self.assertIn("标题及其子标题整体下移一级", normalized_class)
|
||||
self.assertEqual(
|
||||
2,
|
||||
class_section.count("<!-- tags: OpenXml 属性 创建 -->"),
|
||||
)
|
||||
self.assertIn(
|
||||
"所有 class member 都可以包含可选的 `tags`",
|
||||
normalized_standard,
|
||||
)
|
||||
self.assertIn(
|
||||
"### `CreateVirtual(position, row_index, _story)`",
|
||||
class_section,
|
||||
)
|
||||
self.assertIn("声明:class function", class_section)
|
||||
self.assertIn("返回:ParagraphSegment", class_section)
|
||||
|
||||
@unittest.skipUnless(yaml is not None, "pyyaml is not installed")
|
||||
def test_json_and_yaml_examples_use_the_same_complete_structure(self):
|
||||
json_data = json.loads(EXAMPLE_JSON.read_text(encoding="utf-8"))
|
||||
yaml_data = yaml.safe_load(EXAMPLE_YAML.read_text(encoding="utf-8"))
|
||||
|
||||
expected_root = {"module", "path", "declarations"}
|
||||
self.assertEqual(expected_root, set(json_data))
|
||||
self.assertEqual(expected_root, set(yaml_data))
|
||||
self.assertEqual(json_data, yaml_data)
|
||||
|
||||
declarations = {
|
||||
item["kind"]: item for item in json_data["declarations"]
|
||||
}
|
||||
self.assertEqual({"function", "class", "unit"}, set(declarations))
|
||||
self.assertEqual(
|
||||
["class", "function", "unit"],
|
||||
[item["kind"] for item in json_data["declarations"]],
|
||||
)
|
||||
|
||||
class_members = declarations["class"]["members"]
|
||||
self.assertEqual(
|
||||
[
|
||||
"create",
|
||||
"CreateVirtual",
|
||||
"Prefix",
|
||||
"NamespaceUri",
|
||||
"value_",
|
||||
"Count",
|
||||
"DefaultPrefix",
|
||||
"MaximumAttributes",
|
||||
],
|
||||
[member["name"] for member in class_members],
|
||||
)
|
||||
self.assertIn("class", [m.get("binding") for m in class_members])
|
||||
self.assertEqual(
|
||||
"string",
|
||||
next(m for m in class_members if m["name"] == "Prefix")["type"],
|
||||
)
|
||||
self.assertNotIn(
|
||||
"type",
|
||||
next(m for m in class_members if m["name"] == "NamespaceUri"),
|
||||
)
|
||||
self.assertEqual(
|
||||
{"method", "property", "field", "constant"},
|
||||
{member["kind"] for member in class_members},
|
||||
)
|
||||
self.assertTrue(
|
||||
next(m for m in class_members if m["name"] == "Count")["static"]
|
||||
)
|
||||
self.assertTrue(
|
||||
next(
|
||||
m
|
||||
for m in class_members
|
||||
if m["name"] == "MaximumAttributes"
|
||||
)["static"]
|
||||
)
|
||||
|
||||
unit_members = declarations["unit"]["members"]
|
||||
self.assertEqual(
|
||||
{"constant", "variable", "function", "class"},
|
||||
{member["kind"] for member in unit_members},
|
||||
)
|
||||
document = next(
|
||||
member for member in unit_members if member["kind"] == "class"
|
||||
)
|
||||
self.assertEqual(
|
||||
{("method", "instance"), ("method", "class"), ("property", None)},
|
||||
{(m["kind"], m.get("binding")) for m in document["members"]},
|
||||
)
|
||||
|
||||
page = (
|
||||
self.skill_dir
|
||||
/ "references"
|
||||
/ "codegen"
|
||||
/ "project"
|
||||
/ f"{json_data['path']}.md"
|
||||
)
|
||||
markdown_by_format = {}
|
||||
for output_format, recording in (
|
||||
("json", EXAMPLE_JSON),
|
||||
("yaml", EXAMPLE_YAML),
|
||||
):
|
||||
generate = self.run_command(GENERATE, recording, cwd=self.root)
|
||||
self.assertEqual(0, generate.returncode, generate.stderr)
|
||||
markdown_by_format[output_format] = page.read_bytes()
|
||||
lint = self.run_command(LINT, "--file", page, "--strict")
|
||||
self.assertEqual(0, lint.returncode, lint.stdout + lint.stderr)
|
||||
self.assertEqual(markdown_by_format["json"], markdown_by_format["yaml"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user