feat(tsl-api-reference): index class and unit APIs

Migrate reference pages to declaration-line storage and rebuild the
13-column index.

Add qualified member lookup plus regression and end-to-end coverage.
This commit is contained in:
csh
2026-07-29 15:47:46 +08:00
parent 9edc8fc868
commit 13be5ea0aa
239 changed files with 39157 additions and 12846 deletions
+752
View File
@@ -1,5 +1,10 @@
import io
import importlib.util
import subprocess
import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
@@ -10,6 +15,7 @@ SCRIPT = (
/ "scripts"
/ "lookup.py"
)
SKILL_MD = SCRIPT.parents[1] / "SKILL.md"
def load_script():
@@ -20,6 +26,752 @@ def load_script():
class LookupTest(unittest.TestCase):
def write_unified_api_fixture(self, skill_root):
data_dir = skill_root / "data"
codegen_dir = skill_root / "references" / "codegen"
data_dir.mkdir()
(codegen_dir / "project").mkdir(parents=True)
header = [
"name",
"scope",
"module",
"signature",
"page",
"anchor",
"tags",
"summary",
"kind",
"binding",
"visibility",
"owner",
"qualified_name",
]
def row(
name,
signature,
page,
anchor,
summary,
kind,
binding="",
visibility="",
owner="",
qualified_name="",
):
return [
name,
"project",
"demo",
signature,
page,
anchor,
"",
summary,
kind,
binding,
visibility,
owner,
qualified_name or name,
]
rows = [
row(
"DemoUnit",
"DemoUnit",
"project/demo_unit.md",
"demounit",
"接口。",
"unit",
),
row(
"Open",
"Open()",
"project/demo_unit.md",
"open",
"打开。",
"function",
"unit",
"public",
"DemoUnit",
"DemoUnit.Open",
),
row(
"Document",
"Document",
"project/demo_unit.md",
"document",
"文档。",
"class",
"unit",
"public",
"DemoUnit",
"DemoUnit.Document",
),
row(
"Save",
"Save(path)",
"project/demo_unit.md",
"savepath",
"按路径保存。",
"method",
"instance",
"public",
"DemoUnit.Document",
"DemoUnit.Document.Save",
),
row(
"Save",
"Save(mode)",
"project/demo_unit.md",
"savemode",
"按模式保存。",
"method",
"instance",
"protected",
"DemoUnit.Document",
"DemoUnit.Document.Save",
),
row(
"Name",
"Name",
"project/demo_unit.md",
"name",
"文档名称。",
"field",
"instance",
"public",
"DemoUnit.Document",
"DemoUnit.Document.Name",
),
row(
"Other",
"Other",
"project/demo_unit.md",
"other",
"其他对象。",
"class",
"unit",
"public",
"DemoUnit",
"DemoUnit.Other",
),
row(
"Save",
"Save()",
"project/demo_unit.md",
"save",
"保存其他对象。",
"method",
"instance",
"public",
"DemoUnit.Other",
"DemoUnit.Other.Save",
),
row(
"Widget",
"Widget",
"project/widget.md",
"widget",
"组件。",
"class",
),
row(
"Create",
"Create()",
"project/widget.md",
"create",
"创建组件。",
"method",
"class",
"public",
"Widget",
"Widget.Create",
),
row(
"Count",
"Count",
"project/widget.md",
"count",
"组件数量。",
"field",
"static",
"protected",
"Widget",
"Widget.Count",
),
]
index_text = "\n".join(
["\t".join(header), *("\t".join(values) for values in rows)]
)
tsv_path = data_dir / "function_index.tsv"
tsv_path.write_text(index_text + "\n", encoding="utf-8")
(codegen_dir / "project" / "demo_unit.md").write_text(
"# Demo / Unit\n\n"
"## `DemoUnit`\n\n"
"声明:unit\n\n"
"接口。\n\n"
"### `Open()`\n\n"
"声明:function\n\n"
"打开。\n\n"
"返回:Document\n\n"
"### `Document`\n\n"
"声明:class\n\n"
"文档。\n\n"
"#### `Save(path)`\n\n"
"声明:function\n\n"
"按路径保存。\n\n"
"可见性:`public`\n\n"
"#### `Save(mode)`\n\n"
"声明:function\n\n"
"按模式保存。\n\n"
"可见性:`protected`\n\n"
"#### `Name`\n\n"
"声明:field\n\n"
"文档名称。\n\n"
"可见性:`public`\n\n"
"类型:string\n\n"
"### `Other`\n\n"
"声明:class\n\n"
"其他对象。\n\n"
"#### `Save()`\n\n"
"声明:function\n\n"
"保存其他对象。\n\n"
"可见性:`public`\n",
encoding="utf-8",
)
(codegen_dir / "project" / "widget.md").write_text(
"# Demo / Class\n\n"
"## `Widget`\n\n"
"声明:class\n\n"
"组件。\n\n"
"### `Create()`\n\n"
"声明:class function\n\n"
"创建组件。\n\n"
"可见性:`public`\n\n"
"### `Count`\n\n"
"声明:static field\n\n"
"组件数量。\n\n"
"可见性:`protected`\n\n"
"类型:integer\n",
encoding="utf-8",
)
return tsv_path
def write_mixed_api_fixture(self, skill_root):
data_dir = skill_root / "data"
codegen_dir = skill_root / "references" / "codegen" / "project"
data_dir.mkdir()
codegen_dir.mkdir(parents=True)
header = [
"name",
"scope",
"module",
"signature",
"page",
"anchor",
"tags",
"summary",
"kind",
"binding",
"visibility",
"owner",
"qualified_name",
]
page = "project/mixed.md"
rows = [
[
"Widget",
"project",
"mixed",
"Widget",
page,
"widget",
"",
"组件。",
"class",
"",
"",
"",
"Widget",
],
[
"Open",
"project",
"mixed",
"Open()",
page,
"open",
"",
"打开。",
"method",
"instance",
"public",
"Widget",
"Widget.Open",
],
[
"Parse",
"project",
"mixed",
"Parse(text)",
page,
"parse",
"",
"解析。",
"function",
"",
"",
"",
"Parse",
],
[
"Runtime",
"project",
"mixed",
"Runtime",
page,
"runtime",
"",
"接口。",
"unit",
"",
"",
"",
"Runtime",
],
[
"Document",
"project",
"mixed",
"Document",
page,
"document",
"",
"文档。",
"class",
"unit",
"public",
"Runtime",
"Runtime.Document",
],
[
"Save",
"project",
"mixed",
"Save()",
page,
"save",
"",
"保存。",
"method",
"instance",
"public",
"Runtime.Document",
"Runtime.Document.Save",
],
[
"Other",
"project",
"mixed",
"Other",
page,
"other",
"",
"其他组件。",
"class",
"",
"",
"",
"Other",
],
]
tsv_path = data_dir / "function_index.tsv"
tsv_path.write_text(
"\n".join(
["\t".join(header), *("\t".join(row) for row in rows)]
)
+ "\n",
encoding="utf-8",
)
(codegen_dir / "mixed.md").write_text(
"# Mixed\n\n"
"## `Widget`\n\n声明:class\n\n组件。\n\n"
"### `Open()`\n\n声明:function\n\n打开。\n\n可见性:public\n\n"
"## `Parse(text)`\n\n声明:function\n\n解析。\n\n"
"| 参数 | 类型 | 说明 |\n"
"| --- | --- | --- |\n"
"| `text` | string | 文本 |\n\n"
"返回:Widget\n\n"
"### 示例\n\n"
"```markdown\n## `NotBoundary`\n```\n\n"
"## `Runtime`\n\n声明:unit\n\n接口。\n\n"
"### `Document`\n\n声明:class\n\n文档。\n\n"
"#### `Save()`\n\n声明:function\n\n保存。\n\n可见性:public\n\n"
"## `Other`\n\n声明:class\n\n其他组件。\n",
encoding="utf-8",
)
return tsv_path
def run_lookup(self, tsv_path, *args):
return subprocess.run(
[
sys.executable,
str(SCRIPT),
*args,
"--tsv",
str(tsv_path),
],
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
def test_tsv_without_tags_is_rejected(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = Path(temp_dir) / "function_index.tsv"
tsv_path.write_text(
"name\tscope\tmodule\tsignature\tpage\tanchor\tsummary\n"
"foo\tbuiltin\tmisc\tfoo()\tmisc.md\tfoo\tLegacy entry\n",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--kw",
"Legacy",
"--tsv",
str(tsv_path),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(1, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("missing required columns: tags", result.stderr)
self.assertNotIn("Traceback", result.stderr)
def test_missing_required_tsv_columns_are_rejected(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = Path(temp_dir) / "function_index.tsv"
tsv_path.write_text("name\nfoo\n", encoding="utf-8")
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--name",
"foo",
"--tsv",
str(tsv_path),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(1, result.returncode)
self.assertIn("missing required columns", result.stderr)
self.assertNotIn("Traceback", result.stderr)
def test_tsv_row_width_is_validated(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = Path(temp_dir) / "function_index.tsv"
tsv_path.write_text(
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
"foo\tbuiltin\n",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--name",
"foo",
"--tsv",
str(tsv_path),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(1, result.returncode)
self.assertIn("line 2 has 2 columns; expected 8", result.stderr)
self.assertNotIn("Traceback", result.stderr)
def test_missing_entry_body_is_rejected(self):
with tempfile.TemporaryDirectory() as temp_dir:
skill_root = Path(temp_dir)
data_dir = skill_root / "data"
codegen_dir = skill_root / "references" / "codegen"
data_dir.mkdir()
codegen_dir.mkdir(parents=True)
(data_dir / "function_index.tsv").write_text(
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
"foo\tbuiltin\tmisc\tfoo()\tmissing.md\tfoo\t\tFallback only\n",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--name",
"foo",
"--tsv",
str(data_dir / "function_index.tsv"),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(1, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("entry body not found", result.stderr)
self.assertIn("missing.md#foo", result.stderr)
def test_empty_queries_are_rejected(self):
for option in ("--name", "--kw"):
with self.subTest(option=option):
result = subprocess.run(
[sys.executable, str(SCRIPT), option, ""],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(2, result.returncode)
self.assertIn("must not be empty", result.stderr)
def test_non_positive_limit_is_rejected(self):
for limit in ("0", "-1"):
with self.subTest(limit=limit):
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--kw",
"数组",
"--limit",
limit,
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(2, result.returncode)
self.assertIn("--limit must be >= 1", result.stderr)
def test_exact_search_uses_anchor_for_duplicate_signatures(self):
module = load_script()
with tempfile.TemporaryDirectory() as temp_dir:
skill_root = Path(temp_dir)
data_dir = skill_root / "data"
codegen_dir = skill_root / "references" / "codegen"
data_dir.mkdir()
codegen_dir.mkdir(parents=True)
header = [
"name",
"scope",
"module",
"signature",
"page",
"anchor",
"tags",
"summary",
]
rows = [
[
"CAPA",
"builtin",
"language",
"CAPA()",
"objects.md",
"capa",
"",
"IMAP summary",
],
[
"CAPA",
"builtin",
"language",
"CAPA()",
"objects.md",
"capa-1",
"",
"Pop3 summary",
],
]
index_text = "\n".join(
["\t".join(header), *("\t".join(row) for row in rows)]
)
(data_dir / "function_index.tsv").write_text(
index_text + "\n", encoding="utf-8"
)
(codegen_dir / "objects.md").write_text(
"# Objects\n\n"
"## `CAPA()`\n\n"
"声明:function\n\n"
"对象:IMAP\n\n"
"## `CAPA()`\n\n"
"声明:function\n\n"
"对象:Pop3\n\n"
"",
encoding="utf-8",
)
stdout = io.StringIO()
with redirect_stdout(stdout):
status = module.main(
[
"--name",
"CAPA",
"--tsv",
str(data_dir / "function_index.tsv"),
]
)
output = stdout.getvalue()
self.assertEqual(0, status)
self.assertEqual(1, output.count("对象:IMAP"))
self.assertEqual(1, output.count("对象:Pop3"))
self.assertIn("objects.md#capa -->", output)
self.assertIn("objects.md#capa-1 -->", output)
def test_exact_qualified_member_query_returns_only_member_overloads(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
result = self.run_lookup(
tsv_path, "--name", "demounit.document.save"
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(2, result.stdout.count("#### `Save("))
self.assertNotIn("#### `Name`", result.stdout)
self.assertNotIn("### `Other`", result.stdout)
def test_exact_unit_class_query_returns_its_h4_subtree_only(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
result = self.run_lookup(
tsv_path, "--name", "DemoUnit.Document"
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("### `Document`", result.stdout)
self.assertIn("#### `Save(path)`", result.stdout)
self.assertIn("#### `Name`", result.stdout)
self.assertNotIn("### `Other`", result.stdout)
def test_exact_unit_query_returns_the_complete_unit_subtree(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
result = self.run_lookup(tsv_path, "--name", "DemoUnit")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("## `DemoUnit`", result.stdout)
self.assertIn("### `Open()`", result.stdout)
self.assertIn("### `Document`", result.stdout)
self.assertIn("#### `Save(path)`", result.stdout)
self.assertIn("### `Other`", result.stdout)
self.assertNotIn("## `Widget`", result.stdout)
def test_exact_class_query_returns_all_class_members(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
result = self.run_lookup(tsv_path, "--name", "widget")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("## `Widget`", result.stdout)
self.assertIn("### `Create()`\n\n声明:class function", result.stdout)
self.assertIn("### `Count`\n\n声明:static field", result.stdout)
self.assertNotIn("## `DemoUnit`", result.stdout)
def test_exact_class_in_mixed_page_stops_at_next_h2(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
result = self.run_lookup(tsv_path, "--name", "Widget")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("### `Open()`", result.stdout)
self.assertNotIn("## `Parse(text)`", result.stdout)
def test_exact_function_in_mixed_page_keeps_h3_example_only(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
result = self.run_lookup(tsv_path, "--name", "Parse")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("### 示例", result.stdout)
self.assertIn("## `NotBoundary`", result.stdout)
self.assertNotIn("## `Runtime`", result.stdout)
def test_exact_unit_in_mixed_page_stops_before_following_class(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
result = self.run_lookup(tsv_path, "--name", "Runtime")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("#### `Save()`", result.stdout)
self.assertNotIn("## `Other`", result.stdout)
def test_unqualified_member_query_returns_all_owners(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
result = self.run_lookup(tsv_path, "--name", "Save")
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(3, result.stdout.count("#### `Save("))
self.assertIn("按路径保存。", result.stdout)
self.assertIn("按模式保存。", result.stdout)
self.assertIn("保存其他对象。", result.stdout)
def test_keyword_search_includes_unified_fields_and_declaration_term(self):
with tempfile.TemporaryDirectory() as temp_dir:
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
qualified = self.run_lookup(
tsv_path, "--kw", "Widget.Create"
)
visibility = self.run_lookup(
tsv_path, "--kw", "protected", "method"
)
self.assertEqual(0, qualified.returncode, qualified.stderr)
self.assertIn("Widget.Create\tclass function\tCreate()", qualified.stdout)
self.assertNotIn("static function", qualified.stdout)
self.assertEqual(0, visibility.returncode, visibility.stderr)
self.assertIn("DemoUnit.Document.Save", visibility.stdout)
def test_skill_documents_qualified_queries_and_legacy_index_compatibility(self):
text = SKILL_MD.read_text(encoding="utf-8")
self.assertIn("完全限定名称", text)
self.assertIn("qualified_name", text)
self.assertIn("owner、kind、binding、visibility", text)
self.assertIn("class function", text)
self.assertIn("旧 8 列", text)
def test_keyword_search_includes_tags_and_summary(self):
module = load_script()
rows = [
+405
View File
@@ -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()