387 lines
15 KiB
Python
387 lines
15 KiB
Python
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, "--file", 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(
|
|
"generate.py --file tmp/my-api.yaml",
|
|
readme,
|
|
)
|
|
self.assertIn(
|
|
"generate.py --file tmp/my-api.json",
|
|
readme,
|
|
)
|
|
self.assertIn(
|
|
"generate.py --dir tmp/api-recordings",
|
|
readme,
|
|
)
|
|
self.assertIn("只处理目录中的直属文件", readme)
|
|
self.assertNotIn("generate.py tmp/my-api.yaml", readme)
|
|
self.assertNotIn("generate.py tmp/my-api.json", readme)
|
|
self.assertNotIn("已废弃,请使用 --file", 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, "--file", 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()
|