@@ -16,6 +16,7 @@ SCRIPT = (
|
||||
/ "lookup.py"
|
||||
)
|
||||
SKILL_MD = SCRIPT.parents[1] / "SKILL.md"
|
||||
API_WORKFLOW = SCRIPT.parents[1] / "workflows" / "api-lookup.md"
|
||||
|
||||
|
||||
def load_script():
|
||||
@@ -573,7 +574,7 @@ class LookupTest(unittest.TestCase):
|
||||
self.assertEqual(2, result.returncode)
|
||||
self.assertIn("--limit must be >= 1", result.stderr)
|
||||
|
||||
def test_help_lists_all_bundled_scopes(self):
|
||||
def test_help_derives_scopes_from_selected_index(self):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--help"],
|
||||
capture_output=True,
|
||||
@@ -582,7 +583,9 @@ class LookupTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("builtin、dotnet、third 与 deprecated", result.stdout)
|
||||
self.assertIn("可用值从当前 function_index.tsv 读取", result.stdout)
|
||||
self.assertNotIn("deprecated", result.stdout)
|
||||
self.assertNotIn("--scope third", result.stdout)
|
||||
|
||||
def test_exact_search_uses_anchor_for_duplicate_signatures(self):
|
||||
module = load_script()
|
||||
@@ -775,14 +778,98 @@ class LookupTest(unittest.TestCase):
|
||||
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")
|
||||
skill_text = SKILL_MD.read_text(encoding="utf-8")
|
||||
text = API_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("workflows/api-lookup.md", skill_text)
|
||||
self.assertIn("完全限定名称", text)
|
||||
self.assertIn("--scope third", text)
|
||||
self.assertIn("--scope module", text)
|
||||
self.assertIn("qualified_name", text)
|
||||
self.assertIn("owner、kind、binding、visibility", text)
|
||||
self.assertIn("owner、kind、", text)
|
||||
self.assertIn("binding 和 visibility", text)
|
||||
self.assertIn("class function", text)
|
||||
self.assertIn("旧 8 列", text)
|
||||
self.assertIn("8 列索引", text)
|
||||
self.assertIn("13 列索引", text)
|
||||
|
||||
def test_skill_routes_workflows_and_keeps_stop_resident(self):
|
||||
skill_text = SKILL_MD.read_text(encoding="utf-8")
|
||||
skill_root = SKILL_MD.parent
|
||||
instruction_paths = sorted((skill_root / "workflows").glob("*.md"))
|
||||
|
||||
for path in instruction_paths:
|
||||
self.assertIn(path.relative_to(skill_root).as_posix(), skill_text)
|
||||
self.assertIn("🔴 CHECKPOINT · 🛑 STOP", skill_text)
|
||||
self.assertIn("rc=1 不进入上述“API 缺失”分支", skill_text)
|
||||
self.assertIn("原样名称或 `qualified_name`", skill_text)
|
||||
self.assertIn("`profile_status=not_profiled`", skill_text)
|
||||
|
||||
def test_skill_instruction_package_has_no_reverse_dependencies(self):
|
||||
skill_root = SKILL_MD.parent
|
||||
package_text = "\n".join(
|
||||
path.read_text(encoding="utf-8")
|
||||
for path in [SKILL_MD, *sorted((skill_root / "workflows").glob("*.md"))]
|
||||
)
|
||||
|
||||
for forbidden in (
|
||||
"tsl-syntax-reference",
|
||||
"references/maintenance.md",
|
||||
"tools/tsl-codegen",
|
||||
"scripts/build_dictionary.py",
|
||||
"AGENTS.md",
|
||||
"CONTEXT.md",
|
||||
"docs/adr/",
|
||||
):
|
||||
self.assertNotIn(forbidden, package_text)
|
||||
|
||||
def test_repository_maintenance_doc_owns_api_data_maintenance(self):
|
||||
readme = (
|
||||
SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "MAINTENANCE.md"
|
||||
).read_text(encoding="utf-8")
|
||||
for required in (
|
||||
"TSL API 数据维护",
|
||||
"class_lookup.py --check",
|
||||
"tools/tsl-codegen/scripts/build_index.py",
|
||||
"--skill-dir skills/tsl-api-reference",
|
||||
"tools/tsl-codegen/scripts/build_dictionary.py",
|
||||
"beautifulsoup4",
|
||||
"profile_status=not_profiled",
|
||||
):
|
||||
self.assertIn(required, readme)
|
||||
|
||||
user_readme = (
|
||||
SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "README.md"
|
||||
).read_text(encoding="utf-8")
|
||||
for maintainer_only in (
|
||||
"class_lookup.py --check",
|
||||
"beautifulsoup4",
|
||||
"--source-root <exported-html-dir>",
|
||||
):
|
||||
self.assertNotIn(maintainer_only, user_readme)
|
||||
self.assertIn("profile_status=not_profiled", user_readme)
|
||||
|
||||
def test_public_cli_help_owns_status_and_exit_contracts(self):
|
||||
skill_root = SKILL_MD.parent
|
||||
expected = {
|
||||
"lookup.py": ("退出码:", "--name 无匹配", "page#anchor"),
|
||||
"class_lookup.py": ("输出状态:", "contract_status", "退出码:"),
|
||||
"dictionary_lookup.py": ("status: ambiguous", "status: no_match", "退出码:"),
|
||||
}
|
||||
|
||||
for script_name, fragments in expected.items():
|
||||
with self.subTest(script=script_name):
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(skill_root / "scripts" / script_name),
|
||||
"--help",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
for fragment in fragments:
|
||||
self.assertIn(fragment, result.stdout)
|
||||
|
||||
def test_keyword_search_includes_tags_and_summary(self):
|
||||
module = load_script()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -11,6 +12,8 @@ try:
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
BS4_AVAILABLE = importlib.util.find_spec("bs4") is not None
|
||||
|
||||
|
||||
TOOL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = TOOL_ROOT.parents[1]
|
||||
@@ -18,12 +21,28 @@ 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"
|
||||
BUILD_DICTIONARY = TOOL_ROOT / "scripts" / "build_dictionary.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"
|
||||
API_WORKFLOW = (
|
||||
REPO_ROOT / "skills" / "tsl-api-reference" / "workflows" / "api-lookup.md"
|
||||
)
|
||||
|
||||
|
||||
def load_build_dictionary():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"tsl_codegen_build_dictionary", BUILD_DICTIONARY
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot load {BUILD_DICTIONARY}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class UnifiedPipelineTest(unittest.TestCase):
|
||||
@@ -35,6 +54,33 @@ class UnifiedPipelineTest(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
@unittest.skipUnless(BS4_AVAILABLE, "beautifulsoup4 is not installed")
|
||||
def test_dictionary_notes_escape_source_numbering(self):
|
||||
module = load_build_dictionary()
|
||||
page = module.DictionaryPage(
|
||||
kind="table",
|
||||
scope="macro",
|
||||
title="示例",
|
||||
source_path=("宏观",),
|
||||
source_file="example.html",
|
||||
table=module.DictionaryTable(
|
||||
name="示例",
|
||||
table_id="1",
|
||||
extract_method="",
|
||||
access_code="",
|
||||
update_info=("1、更新频率:月度",),
|
||||
),
|
||||
fields=(),
|
||||
notes=("1. 第一条说明", "2. 第二条说明"),
|
||||
examples=(),
|
||||
)
|
||||
|
||||
rendered = module.render_dictionary_page(page)
|
||||
|
||||
self.assertIn("- 1\\. 第一条说明", rendered)
|
||||
self.assertIn("- 2\\. 第二条说明", rendered)
|
||||
self.assertIn("- 1、更新频率:月度", rendered)
|
||||
|
||||
def run_command(self, *args, cwd=None):
|
||||
return subprocess.run(
|
||||
[sys.executable, *map(str, args)],
|
||||
@@ -212,6 +258,7 @@ class UnifiedPipelineTest(unittest.TestCase):
|
||||
standard = STANDARD.read_text(encoding="utf-8")
|
||||
readme = README.read_text(encoding="utf-8")
|
||||
skill = SKILL.read_text(encoding="utf-8")
|
||||
api_workflow = API_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
self.assertTrue(standard.startswith("# TSL API 文档标准\n"))
|
||||
self.assertNotIn("根字段 `functions`、`class`、`unit`", standard)
|
||||
@@ -273,8 +320,9 @@ class UnifiedPipelineTest(unittest.TestCase):
|
||||
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)
|
||||
self.assertIn("workflows/api-lookup.md", skill)
|
||||
self.assertIn("混合页面", api_workflow)
|
||||
self.assertIn("page#anchor", api_workflow)
|
||||
|
||||
def test_standard_class_methods_reuse_top_level_function_structure(self):
|
||||
standard = STANDARD.read_text(encoding="utf-8")
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SKILL_ROOT = ROOT / "skills" / "tsl-api-reference"
|
||||
LOOKUP = SKILL_ROOT / "scripts" / "lookup.py"
|
||||
DICTIONARY_LOOKUP = SKILL_ROOT / "scripts" / "dictionary_lookup.py"
|
||||
|
||||
|
||||
class ValueDomainCliTest(unittest.TestCase):
|
||||
def run_cli(self, script, *args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(script), *args],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_dictionary_query_resolves_classification_by_label_and_code(self):
|
||||
for query in ("申万煤炭", "SWHY740000"):
|
||||
with self.subTest(query=query):
|
||||
result = self.run_cli(DICTIONARY_LOOKUP, "--query", query)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("status: ok", result.stdout)
|
||||
self.assertIn("类型:参数取值", result.stdout)
|
||||
self.assertIn(
|
||||
"参数域:classification_code(分类属性代码)",
|
||||
result.stdout,
|
||||
)
|
||||
self.assertIn("取值:SWHY740000", result.stdout)
|
||||
self.assertIn("名称:申万煤炭", result.stdout)
|
||||
self.assertIn("父级:SWHY(申万行业)", result.stdout)
|
||||
self.assertIn("层级:1", result.stdout)
|
||||
self.assertIn("关联表:138 股票.股票行业分类信息", result.stdout)
|
||||
self.assertIn("快照日期:2023-10-13", result.stdout)
|
||||
self.assertIn("来源:faq:31494", result.stdout)
|
||||
|
||||
def test_dictionary_query_uses_strong_values_without_hijacking_table_names(self):
|
||||
for query in ("SWHY740000 是什么", "申万煤炭分类代码"):
|
||||
with self.subTest(query=query):
|
||||
result = self.run_cli(DICTIONARY_LOOKUP, "--query", query)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("类型:参数取值", result.stdout)
|
||||
self.assertIn("取值:SWHY740000", result.stdout)
|
||||
|
||||
table = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万行业配置")
|
||||
|
||||
self.assertEqual(0, table.returncode, table.stderr)
|
||||
self.assertIn("类型:table", table.stdout)
|
||||
self.assertIn("表 ID:629", table.stdout)
|
||||
self.assertNotIn("类型:参数取值", table.stdout)
|
||||
|
||||
def test_pattern_punctuation_is_significant(self):
|
||||
pattern = self.run_cli(DICTIONARY_LOOKUP, "--query", ".N")
|
||||
single_letter = self.run_cli(DICTIONARY_LOOKUP, "--query", "N")
|
||||
|
||||
self.assertEqual(0, pattern.returncode, pattern.stderr)
|
||||
self.assertIn("类型:参数取值", pattern.stdout)
|
||||
self.assertIn("取值:<分类属性代码>.N", pattern.stdout)
|
||||
self.assertEqual(0, single_letter.returncode, single_letter.stderr)
|
||||
self.assertNotIn("类型:参数取值", single_letter.stdout)
|
||||
|
||||
def test_shared_board_type_enum_maps_multiple_api_parameters(self):
|
||||
label = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万三级行业")
|
||||
numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "板块类别 7")
|
||||
bare_numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "7")
|
||||
api = self.run_cli(LOOKUP, "--name", "getbktypelist")
|
||||
bare_lookup = self.run_cli(LOOKUP, "--kw", "7")
|
||||
|
||||
self.assertEqual(0, label.returncode, label.stderr)
|
||||
self.assertIn("status: ambiguous", label.stdout)
|
||||
self.assertIn("取值:7", label.stdout)
|
||||
self.assertIn("名称:申万三级行业", label.stdout)
|
||||
self.assertIn("绑定 API:getbktypelist.bktype; stocksbklist.bktype", label.stdout)
|
||||
self.assertIn("字段:申万三级行业", label.stdout)
|
||||
self.assertEqual(0, numeric.returncode, numeric.stderr)
|
||||
self.assertIn("名称:申万三级行业", numeric.stdout)
|
||||
self.assertEqual(0, bare_numeric.returncode, bare_numeric.stderr)
|
||||
self.assertNotIn("类型:参数取值", bare_numeric.stdout)
|
||||
self.assertEqual(0, api.returncode, api.stderr)
|
||||
self.assertIn("market_board_type", api.stdout)
|
||||
self.assertIn("完整", api.stdout)
|
||||
self.assertIn("`7` — 申万三级行业", api.stdout)
|
||||
self.assertEqual(0, bare_lookup.returncode, bare_lookup.stderr)
|
||||
self.assertNotIn("参数取值域:getbktypelist.bktype", bare_lookup.stdout)
|
||||
|
||||
def test_historical_market_board_codes_use_the_shared_domain(self):
|
||||
dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "TSI000001")
|
||||
lookup = self.run_cli(LOOKUP, "--kw", "TSI000001")
|
||||
|
||||
self.assertEqual(0, dictionary.returncode, dictionary.stderr)
|
||||
self.assertIn("取值:TSI000001", dictionary.stdout)
|
||||
self.assertIn("名称:A股板块", dictionary.stdout)
|
||||
self.assertIn("绑定 API:getBkByDate.index_id", dictionary.stdout)
|
||||
self.assertEqual(0, lookup.returncode, lookup.stderr)
|
||||
self.assertIn("历史市场板块代码:TSI000001(A股板块)", lookup.stdout)
|
||||
self.assertNotIn("参数取值域:getbktypelist.bktype", lookup.stdout)
|
||||
|
||||
def test_exact_code_value_ignores_incidental_dictionary_id_substrings(self):
|
||||
result = self.run_cli(DICTIONARY_LOOKUP, "--query", "SWHY740000")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("status: ok", result.stdout)
|
||||
self.assertIn("取值:SWHY740000", result.stdout)
|
||||
self.assertNotIn("字段:截止日", result.stdout)
|
||||
|
||||
def test_named_historical_board_domain_is_complete_and_api_specific(self):
|
||||
dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "中小企业板")
|
||||
lookup = self.run_cli(LOOKUP, "--kw", "中小企业板")
|
||||
exact = self.run_cli(LOOKUP, "--name", "getAbkbyDate")
|
||||
|
||||
self.assertEqual(0, dictionary.returncode, dictionary.stderr)
|
||||
self.assertIn("绑定 API:getAbkbyDate.bk_name", dictionary.stdout)
|
||||
self.assertIn("2021-04-06 并入主板", dictionary.stdout)
|
||||
self.assertEqual(0, lookup.returncode, lookup.stderr)
|
||||
self.assertIn("历史市场板块名:中小企业板", lookup.stdout)
|
||||
self.assertEqual(0, exact.returncode, exact.stderr)
|
||||
self.assertIn("historical_a_share_board_name", exact.stdout)
|
||||
self.assertIn("`中小企业板` — 中小企业板(历史兼容)", exact.stdout)
|
||||
|
||||
def test_scope_filters_multi_table_value_domains(self):
|
||||
stock = self.run_cli(
|
||||
DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "stock"
|
||||
)
|
||||
fund = self.run_cli(
|
||||
DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "fund"
|
||||
)
|
||||
|
||||
self.assertEqual(0, stock.returncode, stock.stderr)
|
||||
self.assertIn("关联表:138 股票.股票行业分类信息", stock.stdout)
|
||||
self.assertNotIn("355 基金.基金分类信息", stock.stdout)
|
||||
self.assertEqual(0, fund.returncode, fund.stderr)
|
||||
self.assertIn("关联表:355 基金.基金分类信息", fund.stdout)
|
||||
self.assertNotIn("138 股票.股票行业分类信息", fund.stdout)
|
||||
|
||||
def test_keyword_query_maps_value_to_current_and_historical_apis(self):
|
||||
result = self.run_cli(LOOKUP, "--kw", "申万", "煤炭")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("getBk\tfunction\tgetBk(marketlist)", result.stdout)
|
||||
self.assertIn(
|
||||
"getBkByDate\tfunction\tgetBkByDate(index_id, end_t, extype)",
|
||||
result.stdout,
|
||||
)
|
||||
self.assertIn("getBk.marketlist", result.stdout)
|
||||
self.assertIn("当前板块候选:申万煤炭", result.stdout)
|
||||
self.assertIn("运行时核验", result.stdout)
|
||||
self.assertIn("getBkByDate.index_id", result.stdout)
|
||||
self.assertIn("历史分类代码:SWHY740000", result.stdout)
|
||||
|
||||
def test_keyword_annotations_only_include_the_top_value_group(self):
|
||||
exact_code = self.run_cli(LOOKUP, "--kw", "SWHY740000")
|
||||
domain_query = self.run_cli(LOOKUP, "--kw", "属性", "代码")
|
||||
|
||||
self.assertEqual(0, exact_code.returncode, exact_code.stderr)
|
||||
self.assertIn("历史分类代码:SWHY740000", exact_code.stdout)
|
||||
self.assertNotIn("历史分类代码:SWHY(申万行业)", exact_code.stdout)
|
||||
self.assertEqual(0, domain_query.returncode, domain_query.stderr)
|
||||
self.assertIn("目录选择器:属性代码", domain_query.stdout)
|
||||
self.assertIn("下级分类代码模式:<分类属性代码>.N", domain_query.stdout)
|
||||
self.assertNotIn("历史分类代码:CAPCHY", domain_query.stdout)
|
||||
|
||||
def test_exact_getbk_query_explains_runtime_catalog_boundary(self):
|
||||
result = self.run_cli(LOOKUP, "--name", "getBk")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("参数取值域", result.stdout)
|
||||
self.assertIn("marketlist", result.stdout)
|
||||
self.assertIn("runtime_catalog", result.stdout)
|
||||
self.assertIn("静态记录不完整", result.stdout)
|
||||
self.assertIn("getBkList2", result.stdout)
|
||||
self.assertIn("getUserBkList2", result.stdout)
|
||||
self.assertIn("按值查询", result.stdout)
|
||||
self.assertNotIn("`申万煤炭`", result.stdout)
|
||||
|
||||
def test_runtime_domain_query_returns_resolvers_not_unrelated_market_codes(self):
|
||||
result = self.run_cli(DICTIONARY_LOOKUP, "--query", "market_board")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("status: ok", result.stdout)
|
||||
self.assertIn("类型:参数取值域", result.stdout)
|
||||
self.assertIn("参数域:market_board", result.stdout)
|
||||
self.assertIn("绑定 API:getBk.marketlist", result.stdout)
|
||||
self.assertIn("getBkList2(bktype)(系统目录)", result.stdout)
|
||||
self.assertIn("getUserBkList2(bktype)(用户目录)", result.stdout)
|
||||
self.assertIn("申万煤炭", result.stdout)
|
||||
self.assertNotIn("TSI000001", result.stdout)
|
||||
|
||||
def test_domain_label_query_returns_one_domain_summary(self):
|
||||
result = self.run_cli(DICTIONARY_LOOKUP, "--query", "分类属性代码")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("status: ok", result.stdout)
|
||||
self.assertIn("参数域:classification_code", result.stdout)
|
||||
self.assertIn("绑定 API:getBkByDate.index_id", result.stdout)
|
||||
self.assertIn("SWHY740000(申万煤炭)", result.stdout)
|
||||
self.assertNotIn("类型:field", result.stdout)
|
||||
|
||||
def test_exact_api_query_does_not_expand_versioned_catalog(self):
|
||||
result = self.run_cli(LOOKUP, "--name", "getBkByDate")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("分类属性代码", result.stdout)
|
||||
self.assertIn("已记录值或候选:7 条", result.stdout)
|
||||
self.assertIn("dictionary_lookup.py --query", result.stdout)
|
||||
parameter_domain = result.stdout.split("### 参数取值域", maxsplit=1)[1]
|
||||
self.assertNotIn("`SWHY740000`", parameter_domain)
|
||||
|
||||
def test_invalid_value_domain_data_is_a_deployment_error(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
invalid = Path(temp_dir) / "value_domains.json"
|
||||
invalid.write_text('{"version": 1, "domains": []}\n', encoding="utf-8")
|
||||
|
||||
for script, action in (
|
||||
(LOOKUP, ("--kw", "申万")),
|
||||
(DICTIONARY_LOOKUP, ("--query", "申万")),
|
||||
):
|
||||
with self.subTest(script=script.name):
|
||||
result = self.run_cli(
|
||||
script,
|
||||
*action,
|
||||
"--value-domains",
|
||||
str(invalid),
|
||||
)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertEqual("", result.stdout)
|
||||
self.assertIn("domains must not be empty", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_parameter_domain_binding_does_not_cross_scope_or_module(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
data_dir = Path(temp_dir) / "data"
|
||||
data_dir.mkdir()
|
||||
tsv = data_dir / "function_index.tsv"
|
||||
tsv.write_text(
|
||||
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
||||
"getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t"
|
||||
"dotnet/market.md\tgetbk\t\t系统板块\n"
|
||||
"getBk\tdotnet\tdatawarehouse\tgetBk()\t"
|
||||
"dotnet/market.md\tgetbk-empty\t\t无参数重载\n"
|
||||
"getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t"
|
||||
"getbk\t\t项目函数\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
domains = data_dir / "value_domains.json"
|
||||
domains.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"domains": [
|
||||
{
|
||||
"id": "market_board",
|
||||
"label": "市场板块",
|
||||
"mode": "runtime_catalog",
|
||||
"complete": False,
|
||||
"bindings": [
|
||||
{
|
||||
"scope": "dotnet",
|
||||
"module": "datawarehouse",
|
||||
"api": "getBk",
|
||||
"parameter": "marketlist",
|
||||
"role": "current_components",
|
||||
}
|
||||
],
|
||||
"resolvers": [
|
||||
{
|
||||
"scope": "dotnet",
|
||||
"module": "datawarehouse",
|
||||
"api": "getBkList2",
|
||||
"parameter": "bktype",
|
||||
"catalog": "system",
|
||||
"source": "net_function:28964",
|
||||
}
|
||||
],
|
||||
"related_tables": [],
|
||||
"values": [
|
||||
{
|
||||
"value": "申万煤炭",
|
||||
"label": "申万煤炭",
|
||||
"sources": ["faq:31494"],
|
||||
}
|
||||
],
|
||||
"sources": ["faq:31494"],
|
||||
"as_of": "2026-08-20",
|
||||
}
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli(
|
||||
LOOKUP,
|
||||
"--kw",
|
||||
"申万煤炭",
|
||||
"--tsv",
|
||||
str(tsv),
|
||||
"--value-domains",
|
||||
str(domains),
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("dotnet/market.md#getbk", result.stdout)
|
||||
self.assertNotIn("dotnet/market.md#getbk-empty", result.stdout)
|
||||
self.assertNotIn("project/demo.md#getbk", result.stdout)
|
||||
|
||||
def test_exact_query_attaches_domain_only_to_overload_with_bound_parameter(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
data_dir = root / "data"
|
||||
codegen = root / "references" / "codegen" / "dotnet"
|
||||
data_dir.mkdir()
|
||||
codegen.mkdir(parents=True)
|
||||
tsv = data_dir / "function_index.tsv"
|
||||
tsv.write_text(
|
||||
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
||||
"getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t"
|
||||
"dotnet/market.md\tgetbk\t\t系统板块\n"
|
||||
"getBk\tdotnet\tdatawarehouse\tgetBk()\t"
|
||||
"dotnet/market.md\tgetbk-1\t\t无参数重载\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(codegen / "market.md").write_text(
|
||||
"# Dotnet\n\n"
|
||||
"## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n\n"
|
||||
"## `getBk()`\n\n声明:function\n\n无参数重载\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
domains = data_dir / "value_domains.json"
|
||||
domains.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"domains": [
|
||||
{
|
||||
"id": "market_board",
|
||||
"label": "市场板块",
|
||||
"mode": "runtime_catalog",
|
||||
"complete": False,
|
||||
"bindings": [
|
||||
{
|
||||
"scope": "dotnet",
|
||||
"module": "datawarehouse",
|
||||
"api": "getBk",
|
||||
"parameter": "marketlist",
|
||||
"role": "current_components",
|
||||
}
|
||||
],
|
||||
"resolvers": [
|
||||
{
|
||||
"scope": "dotnet",
|
||||
"module": "datawarehouse",
|
||||
"api": "getBkList2",
|
||||
"parameter": "bktype",
|
||||
"catalog": "system",
|
||||
"source": "net_function:28964",
|
||||
}
|
||||
],
|
||||
"related_tables": [],
|
||||
"values": [],
|
||||
"sources": ["net_function:28960"],
|
||||
"as_of": "2026-08-20",
|
||||
}
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli(
|
||||
LOOKUP,
|
||||
"--name",
|
||||
"getBk",
|
||||
"--tsv",
|
||||
str(tsv),
|
||||
"--value-domains",
|
||||
str(domains),
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("## `getBk(marketlist)`", result.stdout)
|
||||
self.assertIn("## `getBk()`", result.stdout)
|
||||
self.assertEqual(1, result.stdout.count("### 参数取值域"))
|
||||
|
||||
def test_exact_query_does_not_attach_domain_to_same_name_in_other_scope(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
data_dir = root / "data"
|
||||
codegen = root / "references" / "codegen"
|
||||
(codegen / "dotnet").mkdir(parents=True)
|
||||
(codegen / "project").mkdir(parents=True)
|
||||
data_dir.mkdir()
|
||||
tsv = data_dir / "function_index.tsv"
|
||||
tsv.write_text(
|
||||
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
||||
"getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t"
|
||||
"dotnet/market.md\tgetbk\t\t系统板块\n"
|
||||
"getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t"
|
||||
"getbk\t\t项目函数\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(codegen / "dotnet" / "market.md").write_text(
|
||||
"# Dotnet\n\n## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(codegen / "project" / "demo.md").write_text(
|
||||
"# Project\n\n## `getBk(name)`\n\n声明:function\n\n项目函数\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
domains = data_dir / "value_domains.json"
|
||||
domains.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"domains": [
|
||||
{
|
||||
"id": "market_board",
|
||||
"label": "市场板块",
|
||||
"mode": "runtime_catalog",
|
||||
"complete": False,
|
||||
"bindings": [
|
||||
{
|
||||
"scope": "dotnet",
|
||||
"module": "datawarehouse",
|
||||
"api": "getBk",
|
||||
"parameter": "marketlist",
|
||||
"role": "current_components",
|
||||
}
|
||||
],
|
||||
"resolvers": [
|
||||
{
|
||||
"scope": "dotnet",
|
||||
"module": "datawarehouse",
|
||||
"api": "getBkList2",
|
||||
"parameter": "bktype",
|
||||
"catalog": "system",
|
||||
"source": "net_function:28964",
|
||||
}
|
||||
],
|
||||
"related_tables": [],
|
||||
"values": [],
|
||||
"sources": ["net_function:28960"],
|
||||
"as_of": "2026-08-20",
|
||||
}
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_cli(
|
||||
LOOKUP,
|
||||
"--scope",
|
||||
"project",
|
||||
"--name",
|
||||
"getBk",
|
||||
"--tsv",
|
||||
str(tsv),
|
||||
"--value-domains",
|
||||
str(domains),
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("项目函数", result.stdout)
|
||||
self.assertNotIn("参数取值域", result.stdout)
|
||||
|
||||
def test_bundled_value_domains_reference_existing_apis_parameters_and_tables(self):
|
||||
document = json.loads(
|
||||
(SKILL_ROOT / "data" / "value_domains.json").read_text(encoding="utf-8")
|
||||
)
|
||||
with (SKILL_ROOT / "data" / "function_index.tsv").open(
|
||||
encoding="utf-8", newline=""
|
||||
) as handle:
|
||||
rows = list(csv.DictReader(handle, delimiter="\t"))
|
||||
with (SKILL_ROOT / "data" / "dictionary_index.tsv").open(
|
||||
encoding="utf-8", newline=""
|
||||
) as handle:
|
||||
dictionary_rows = list(csv.DictReader(handle, delimiter="\t"))
|
||||
|
||||
apis = defaultdict(set)
|
||||
for row in rows:
|
||||
match = re.search(r"\((.*)\)", row["signature"])
|
||||
if not match:
|
||||
continue
|
||||
key = (
|
||||
row["scope"].casefold(),
|
||||
row["module"].casefold(),
|
||||
row["name"].casefold(),
|
||||
)
|
||||
for parameter in match.group(1).split(","):
|
||||
parameter = parameter.strip().strip("[]")
|
||||
if parameter:
|
||||
apis[key].add(parameter.casefold())
|
||||
table_pages = {
|
||||
row["table_id"]: row["page"]
|
||||
for row in dictionary_rows
|
||||
if row["kind"] in {"table", "source"} and row["table_id"]
|
||||
}
|
||||
|
||||
for domain in document["domains"]:
|
||||
for binding in domain["bindings"]:
|
||||
self.assert_api_parameter_exists(apis, binding)
|
||||
for resolver in domain["resolvers"]:
|
||||
self.assert_api_parameter_exists(apis, resolver)
|
||||
for table in domain["related_tables"]:
|
||||
self.assertEqual(table["page"], table_pages.get(table["id"]))
|
||||
self.assertTrue((SKILL_ROOT / table["page"]).is_file())
|
||||
for value in domain["values"]:
|
||||
for relation in value.get("relations", []):
|
||||
self.assert_api_parameter_exists(apis, relation["verification"])
|
||||
|
||||
def assert_api_parameter_exists(self, apis, reference):
|
||||
key = (
|
||||
reference["scope"].casefold(),
|
||||
reference["module"].casefold(),
|
||||
(reference.get("api") or reference.get("resolver")).casefold(),
|
||||
)
|
||||
parameter = reference.get("parameter")
|
||||
|
||||
self.assertIn(key, apis)
|
||||
if parameter:
|
||||
self.assertIn(parameter.casefold(), apis[key])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user