code-graph-builder 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_graph_builder/__init__.py +82 -0
- code_graph_builder/builder.py +366 -0
- code_graph_builder/cgb_cli.py +32 -0
- code_graph_builder/cli.py +564 -0
- code_graph_builder/commands_cli.py +1288 -0
- code_graph_builder/config.py +340 -0
- code_graph_builder/constants.py +708 -0
- code_graph_builder/embeddings/__init__.py +40 -0
- code_graph_builder/embeddings/qwen3_embedder.py +573 -0
- code_graph_builder/embeddings/vector_store.py +584 -0
- code_graph_builder/examples/__init__.py +0 -0
- code_graph_builder/examples/example_configuration.py +276 -0
- code_graph_builder/examples/example_kuzu_usage.py +109 -0
- code_graph_builder/examples/example_semantic_search_full.py +347 -0
- code_graph_builder/examples/generate_wiki.py +915 -0
- code_graph_builder/examples/graph_export_example.py +100 -0
- code_graph_builder/examples/rag_example.py +206 -0
- code_graph_builder/examples/test_cli_demo.py +129 -0
- code_graph_builder/examples/test_embedding_api.py +153 -0
- code_graph_builder/examples/test_kuzu_local.py +190 -0
- code_graph_builder/examples/test_rag_redis.py +390 -0
- code_graph_builder/graph_updater.py +605 -0
- code_graph_builder/guidance/__init__.py +1 -0
- code_graph_builder/guidance/agent.py +123 -0
- code_graph_builder/guidance/prompts.py +74 -0
- code_graph_builder/guidance/toolset.py +264 -0
- code_graph_builder/language_spec.py +536 -0
- code_graph_builder/mcp/__init__.py +21 -0
- code_graph_builder/mcp/api_doc_generator.py +764 -0
- code_graph_builder/mcp/file_editor.py +207 -0
- code_graph_builder/mcp/pipeline.py +777 -0
- code_graph_builder/mcp/server.py +161 -0
- code_graph_builder/mcp/tools.py +1800 -0
- code_graph_builder/models.py +115 -0
- code_graph_builder/parser_loader.py +344 -0
- code_graph_builder/parsers/__init__.py +7 -0
- code_graph_builder/parsers/call_processor.py +306 -0
- code_graph_builder/parsers/call_resolver.py +139 -0
- code_graph_builder/parsers/definition_processor.py +796 -0
- code_graph_builder/parsers/factory.py +119 -0
- code_graph_builder/parsers/import_processor.py +293 -0
- code_graph_builder/parsers/structure_processor.py +145 -0
- code_graph_builder/parsers/type_inference.py +143 -0
- code_graph_builder/parsers/utils.py +134 -0
- code_graph_builder/rag/__init__.py +68 -0
- code_graph_builder/rag/camel_agent.py +429 -0
- code_graph_builder/rag/client.py +298 -0
- code_graph_builder/rag/config.py +239 -0
- code_graph_builder/rag/cypher_generator.py +67 -0
- code_graph_builder/rag/llm_backend.py +210 -0
- code_graph_builder/rag/markdown_generator.py +352 -0
- code_graph_builder/rag/prompt_templates.py +440 -0
- code_graph_builder/rag/rag_engine.py +640 -0
- code_graph_builder/rag/review_report.md +172 -0
- code_graph_builder/rag/tests/__init__.py +3 -0
- code_graph_builder/rag/tests/test_camel_agent.py +313 -0
- code_graph_builder/rag/tests/test_client.py +221 -0
- code_graph_builder/rag/tests/test_config.py +177 -0
- code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
- code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
- code_graph_builder/services/__init__.py +39 -0
- code_graph_builder/services/graph_service.py +465 -0
- code_graph_builder/services/kuzu_service.py +665 -0
- code_graph_builder/services/memory_service.py +171 -0
- code_graph_builder/settings.py +75 -0
- code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
- code_graph_builder/tests/__init__.py +1 -0
- code_graph_builder/tests/run_acceptance_check.py +378 -0
- code_graph_builder/tests/test_api_find.py +231 -0
- code_graph_builder/tests/test_api_find_integration.py +226 -0
- code_graph_builder/tests/test_basic.py +78 -0
- code_graph_builder/tests/test_c_api_extraction.py +388 -0
- code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
- code_graph_builder/tests/test_embedder.py +411 -0
- code_graph_builder/tests/test_integration_semantic.py +434 -0
- code_graph_builder/tests/test_mcp_protocol.py +298 -0
- code_graph_builder/tests/test_mcp_user_flow.py +190 -0
- code_graph_builder/tests/test_rag.py +404 -0
- code_graph_builder/tests/test_settings.py +135 -0
- code_graph_builder/tests/test_step1_graph_build.py +264 -0
- code_graph_builder/tests/test_step2_api_docs.py +323 -0
- code_graph_builder/tests/test_step3_embedding.py +278 -0
- code_graph_builder/tests/test_vector_store.py +552 -0
- code_graph_builder/tools/__init__.py +40 -0
- code_graph_builder/tools/graph_query.py +495 -0
- code_graph_builder/tools/semantic_search.py +387 -0
- code_graph_builder/types.py +333 -0
- code_graph_builder/utils/__init__.py +0 -0
- code_graph_builder/utils/path_utils.py +30 -0
- code_graph_builder-0.2.0.dist-info/METADATA +321 -0
- code_graph_builder-0.2.0.dist-info/RECORD +93 -0
- code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
- code_graph_builder-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""阶段二验收检查脚本.
|
|
3
|
+
|
|
4
|
+
执行所有验收检查项并生成报告。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python run_acceptance_check.py [--tinycc-path PATH]
|
|
8
|
+
|
|
9
|
+
选项:
|
|
10
|
+
--tinycc-path PATH TinyCC 项目路径 [默认: /tmp/tinycc]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Sequence
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Colors:
|
|
27
|
+
"""终端颜色."""
|
|
28
|
+
|
|
29
|
+
GREEN = "\033[92m"
|
|
30
|
+
RED = "\033[91m"
|
|
31
|
+
YELLOW = "\033[93m"
|
|
32
|
+
BLUE = "\033[94m"
|
|
33
|
+
RESET = "\033[0m"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def print_header(text: str) -> None:
|
|
37
|
+
"""打印标题."""
|
|
38
|
+
print(f"\n{Colors.BLUE}{'=' * 60}{Colors.RESET}")
|
|
39
|
+
print(f"{Colors.BLUE}{text}{Colors.RESET}")
|
|
40
|
+
print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_success(text: str) -> None:
|
|
44
|
+
"""打印成功信息."""
|
|
45
|
+
print(f"{Colors.GREEN}✓ {text}{Colors.RESET}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_failure(text: str) -> None:
|
|
49
|
+
"""打印失败信息."""
|
|
50
|
+
print(f"{Colors.RED}✗ {text}{Colors.RESET}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def print_warning(text: str) -> None:
|
|
54
|
+
"""打印警告信息."""
|
|
55
|
+
print(f"{Colors.YELLOW}⚠ {text}{Colors.RESET}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_command(cmd: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
|
|
59
|
+
"""运行命令并返回结果."""
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
cmd,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
cwd=cwd,
|
|
66
|
+
timeout=300,
|
|
67
|
+
)
|
|
68
|
+
return result.returncode, result.stdout, result.stderr
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
return -1, "", "Command timed out"
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return -1, "", str(e)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AcceptanceChecker:
|
|
76
|
+
"""验收检查器."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, project_root: Path, tinycc_path: Path) -> None:
|
|
79
|
+
self.project_root = project_root
|
|
80
|
+
self.tinycc_path = tinycc_path
|
|
81
|
+
self.results: dict[str, bool] = {}
|
|
82
|
+
self.errors: dict[str, str] = {}
|
|
83
|
+
|
|
84
|
+
def check_code_quality(self) -> None:
|
|
85
|
+
"""检查代码质量."""
|
|
86
|
+
print_header("代码质量检查")
|
|
87
|
+
|
|
88
|
+
# ruff check
|
|
89
|
+
print("运行 ruff check...")
|
|
90
|
+
returncode, stdout, stderr = run_command(
|
|
91
|
+
["uv", "run", "ruff", "check", "code_graph_builder/"],
|
|
92
|
+
cwd=self.project_root,
|
|
93
|
+
)
|
|
94
|
+
if returncode == 0:
|
|
95
|
+
print_success("ruff check 通过")
|
|
96
|
+
self.results["ruff_check"] = True
|
|
97
|
+
else:
|
|
98
|
+
print_failure("ruff check 失败")
|
|
99
|
+
self.errors["ruff_check"] = stderr or stdout
|
|
100
|
+
self.results["ruff_check"] = False
|
|
101
|
+
|
|
102
|
+
# ruff format check
|
|
103
|
+
print("运行 ruff format --check...")
|
|
104
|
+
returncode, stdout, stderr = run_command(
|
|
105
|
+
["uv", "run", "ruff", "format", "--check", "code_graph_builder/"],
|
|
106
|
+
cwd=self.project_root,
|
|
107
|
+
)
|
|
108
|
+
if returncode == 0:
|
|
109
|
+
print_success("ruff format 通过")
|
|
110
|
+
self.results["ruff_format"] = True
|
|
111
|
+
else:
|
|
112
|
+
print_failure("ruff format 失败")
|
|
113
|
+
self.errors["ruff_format"] = stderr or stdout
|
|
114
|
+
self.results["ruff_format"] = False
|
|
115
|
+
|
|
116
|
+
# ty type check
|
|
117
|
+
print("运行 ty 类型检查...")
|
|
118
|
+
returncode, stdout, stderr = run_command(
|
|
119
|
+
["uv", "run", "ty", "code_graph_builder/"],
|
|
120
|
+
cwd=self.project_root,
|
|
121
|
+
)
|
|
122
|
+
if returncode == 0:
|
|
123
|
+
print_success("ty 类型检查通过")
|
|
124
|
+
self.results["ty_check"] = True
|
|
125
|
+
else:
|
|
126
|
+
print_failure("ty 类型检查失败")
|
|
127
|
+
self.errors["ty_check"] = stderr or stdout
|
|
128
|
+
self.results["ty_check"] = False
|
|
129
|
+
|
|
130
|
+
def check_unit_tests(self) -> None:
|
|
131
|
+
"""检查单元测试."""
|
|
132
|
+
print_header("单元测试检查")
|
|
133
|
+
|
|
134
|
+
print("运行 code_graph_builder 单元测试...")
|
|
135
|
+
returncode, stdout, stderr = run_command(
|
|
136
|
+
[
|
|
137
|
+
"uv",
|
|
138
|
+
"run",
|
|
139
|
+
"pytest",
|
|
140
|
+
"code_graph_builder/tests/",
|
|
141
|
+
"-v",
|
|
142
|
+
"--tb=short",
|
|
143
|
+
],
|
|
144
|
+
cwd=self.project_root,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if returncode == 0:
|
|
148
|
+
print_success("所有单元测试通过")
|
|
149
|
+
self.results["unit_tests"] = True
|
|
150
|
+
else:
|
|
151
|
+
print_failure("单元测试失败")
|
|
152
|
+
self.errors["unit_tests"] = stderr or stdout
|
|
153
|
+
self.results["unit_tests"] = False
|
|
154
|
+
|
|
155
|
+
def check_test_coverage(self) -> None:
|
|
156
|
+
"""检查测试覆盖率."""
|
|
157
|
+
print_header("测试覆盖率检查")
|
|
158
|
+
|
|
159
|
+
print("运行覆盖率测试...")
|
|
160
|
+
returncode, stdout, stderr = run_command(
|
|
161
|
+
[
|
|
162
|
+
"uv",
|
|
163
|
+
"run",
|
|
164
|
+
"pytest",
|
|
165
|
+
"code_graph_builder/tests/",
|
|
166
|
+
"--cov=code_graph_builder",
|
|
167
|
+
"--cov-report=term-missing",
|
|
168
|
+
],
|
|
169
|
+
cwd=self.project_root,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if returncode == 0:
|
|
173
|
+
# 解析覆盖率
|
|
174
|
+
for line in (stdout + stderr).split("\n"):
|
|
175
|
+
if "TOTAL" in line and "%" in line:
|
|
176
|
+
parts = line.split()
|
|
177
|
+
for part in parts:
|
|
178
|
+
if "%" in part:
|
|
179
|
+
try:
|
|
180
|
+
coverage = float(part.replace("%", ""))
|
|
181
|
+
if coverage >= 80:
|
|
182
|
+
print_success(f"测试覆盖率: {coverage}% (>= 80%)")
|
|
183
|
+
self.results["test_coverage"] = True
|
|
184
|
+
else:
|
|
185
|
+
print_failure(f"测试覆盖率: {coverage}% (< 80%)")
|
|
186
|
+
self.results["test_coverage"] = False
|
|
187
|
+
return
|
|
188
|
+
except ValueError:
|
|
189
|
+
continue
|
|
190
|
+
print_warning("无法解析覆盖率数据")
|
|
191
|
+
self.results["test_coverage"] = False
|
|
192
|
+
else:
|
|
193
|
+
print_failure("覆盖率测试失败")
|
|
194
|
+
self.errors["test_coverage"] = stderr or stdout
|
|
195
|
+
self.results["test_coverage"] = False
|
|
196
|
+
|
|
197
|
+
def check_tinycc_parsing(self) -> None:
|
|
198
|
+
"""检查 TinyCC 项目解析."""
|
|
199
|
+
print_header("TinyCC 项目解析检查")
|
|
200
|
+
|
|
201
|
+
if not self.tinycc_path.exists():
|
|
202
|
+
print_warning(f"TinyCC 项目不存在: {self.tinycc_path}")
|
|
203
|
+
print("跳过此检查")
|
|
204
|
+
self.results["tinycc_parse"] = None
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
print(f"解析 TinyCC 项目: {self.tinycc_path}")
|
|
208
|
+
start_time = time.time()
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# 动态导入避免依赖问题
|
|
212
|
+
sys.path.insert(0, str(self.project_root))
|
|
213
|
+
from code_graph_builder.builder import CodeGraphBuilder
|
|
214
|
+
|
|
215
|
+
builder = CodeGraphBuilder(str(self.tinycc_path))
|
|
216
|
+
result = builder.build_graph(clean=True)
|
|
217
|
+
|
|
218
|
+
elapsed_time = time.time() - start_time
|
|
219
|
+
|
|
220
|
+
print(f"解析时间: {elapsed_time:.2f} 秒")
|
|
221
|
+
print(f"创建节点数: {result.nodes_created}")
|
|
222
|
+
print(f"发现函数数: {result.functions_found}")
|
|
223
|
+
print(f"创建关系数: {result.relationships_created}")
|
|
224
|
+
|
|
225
|
+
# 性能检查
|
|
226
|
+
if elapsed_time <= 5.0:
|
|
227
|
+
print_success(f"解析时间达标: {elapsed_time:.2f}s <= 5s")
|
|
228
|
+
self.results["parse_time"] = True
|
|
229
|
+
else:
|
|
230
|
+
print_failure(f"解析时间超标: {elapsed_time:.2f}s > 5s")
|
|
231
|
+
self.results["parse_time"] = False
|
|
232
|
+
|
|
233
|
+
# 功能检查 (TinyCC 大约有 1611 个函数)
|
|
234
|
+
if result.functions_found >= 1400:
|
|
235
|
+
print_success(f"函数识别达标: {result.functions_found} >= 1400")
|
|
236
|
+
self.results["function_count"] = True
|
|
237
|
+
else:
|
|
238
|
+
print_failure(f"函数识别不足: {result.functions_found} < 1400")
|
|
239
|
+
self.results["function_count"] = False
|
|
240
|
+
|
|
241
|
+
# 关系检查
|
|
242
|
+
if result.relationships_created > 0:
|
|
243
|
+
print_success(f"关系创建成功: {result.relationships_created}")
|
|
244
|
+
self.results["relationships"] = True
|
|
245
|
+
else:
|
|
246
|
+
print_failure("未创建任何关系")
|
|
247
|
+
self.results["relationships"] = False
|
|
248
|
+
|
|
249
|
+
self.results["tinycc_parse"] = all([
|
|
250
|
+
self.results.get("parse_time", False),
|
|
251
|
+
self.results.get("function_count", False),
|
|
252
|
+
self.results.get("relationships", False),
|
|
253
|
+
])
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print_failure(f"TinyCC 解析失败: {e}")
|
|
257
|
+
self.errors["tinycc_parse"] = str(e)
|
|
258
|
+
self.results["tinycc_parse"] = False
|
|
259
|
+
|
|
260
|
+
def check_call_resolution_scenarios(self) -> None:
|
|
261
|
+
"""检查调用解析场景测试."""
|
|
262
|
+
print_header("调用解析场景测试")
|
|
263
|
+
|
|
264
|
+
print("运行调用解析场景测试...")
|
|
265
|
+
returncode, stdout, stderr = run_command(
|
|
266
|
+
[
|
|
267
|
+
"uv",
|
|
268
|
+
"run",
|
|
269
|
+
"pytest",
|
|
270
|
+
"code_graph_builder/tests/test_call_resolution_scenarios.py",
|
|
271
|
+
"-v",
|
|
272
|
+
"--tb=short",
|
|
273
|
+
],
|
|
274
|
+
cwd=self.project_root,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if returncode == 0:
|
|
278
|
+
print_success("调用解析场景测试通过")
|
|
279
|
+
self.results["call_resolution"] = True
|
|
280
|
+
else:
|
|
281
|
+
print_failure("调用解析场景测试失败")
|
|
282
|
+
self.errors["call_resolution"] = stderr or stdout
|
|
283
|
+
self.results["call_resolution"] = False
|
|
284
|
+
|
|
285
|
+
def generate_report(self) -> bool:
|
|
286
|
+
"""生成验收报告."""
|
|
287
|
+
print_header("验收报告")
|
|
288
|
+
|
|
289
|
+
# 必须通过的检查项
|
|
290
|
+
required_checks = [
|
|
291
|
+
"ruff_check",
|
|
292
|
+
"ruff_format",
|
|
293
|
+
"ty_check",
|
|
294
|
+
"unit_tests",
|
|
295
|
+
"call_resolution",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
# 可选但重要的检查项
|
|
299
|
+
important_checks = [
|
|
300
|
+
"test_coverage",
|
|
301
|
+
"tinycc_parse",
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
all_passed = True
|
|
305
|
+
|
|
306
|
+
print("必须通过的检查项:")
|
|
307
|
+
for check in required_checks:
|
|
308
|
+
result = self.results.get(check)
|
|
309
|
+
if result is True:
|
|
310
|
+
print_success(f" {check}")
|
|
311
|
+
elif result is False:
|
|
312
|
+
print_failure(f" {check}")
|
|
313
|
+
all_passed = False
|
|
314
|
+
else:
|
|
315
|
+
print_warning(f" {check}: 未执行")
|
|
316
|
+
all_passed = False
|
|
317
|
+
|
|
318
|
+
print("\n重要检查项:")
|
|
319
|
+
for check in important_checks:
|
|
320
|
+
result = self.results.get(check)
|
|
321
|
+
if result is True:
|
|
322
|
+
print_success(f" {check}")
|
|
323
|
+
elif result is False:
|
|
324
|
+
print_failure(f" {check}")
|
|
325
|
+
else:
|
|
326
|
+
print_warning(f" {check}: 未执行/跳过")
|
|
327
|
+
|
|
328
|
+
print("\n" + "=" * 60)
|
|
329
|
+
if all_passed:
|
|
330
|
+
print_success("阶段二验收通过!")
|
|
331
|
+
else:
|
|
332
|
+
print_failure("阶段二验收未通过,请修复上述问题")
|
|
333
|
+
print("=" * 60)
|
|
334
|
+
|
|
335
|
+
# 显示错误详情
|
|
336
|
+
if self.errors:
|
|
337
|
+
print("\n错误详情:")
|
|
338
|
+
for check, error in self.errors.items():
|
|
339
|
+
print(f"\n{Colors.YELLOW}{check}:{Colors.RESET}")
|
|
340
|
+
print(error[:500]) # 限制错误输出长度
|
|
341
|
+
|
|
342
|
+
return all_passed
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
346
|
+
"""主函数."""
|
|
347
|
+
parser = argparse.ArgumentParser(description="阶段二验收检查")
|
|
348
|
+
parser.add_argument(
|
|
349
|
+
"--tinycc-path",
|
|
350
|
+
type=Path,
|
|
351
|
+
default=Path("/tmp/tinycc"),
|
|
352
|
+
help="TinyCC 项目路径 (默认: /tmp/tinycc)",
|
|
353
|
+
)
|
|
354
|
+
args = parser.parse_args(argv)
|
|
355
|
+
|
|
356
|
+
project_root = Path(__file__).parent.parent.parent
|
|
357
|
+
|
|
358
|
+
print_header("Code Graph Builder 阶段二验收检查")
|
|
359
|
+
print(f"项目根目录: {project_root}")
|
|
360
|
+
print(f"TinyCC 路径: {args.tinycc_path}")
|
|
361
|
+
|
|
362
|
+
checker = AcceptanceChecker(project_root, args.tinycc_path)
|
|
363
|
+
|
|
364
|
+
# 执行所有检查
|
|
365
|
+
checker.check_code_quality()
|
|
366
|
+
checker.check_unit_tests()
|
|
367
|
+
checker.check_test_coverage()
|
|
368
|
+
checker.check_call_resolution_scenarios()
|
|
369
|
+
checker.check_tinycc_parsing()
|
|
370
|
+
|
|
371
|
+
# 生成报告
|
|
372
|
+
passed = checker.generate_report()
|
|
373
|
+
|
|
374
|
+
return 0 if passed else 1
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
sys.exit(main())
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Tests for api-find / find_api aggregation logic.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- API doc generator: _sanitise_filename, _render_func_detail
|
|
5
|
+
- cmd_api_find CLI: result structure, API doc attachment
|
|
6
|
+
- _handle_find_api MCP: result structure, API doc attachment
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Unit tests for api_doc_generator helpers
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestSanitiseFilename:
|
|
22
|
+
def test_forward_slash(self):
|
|
23
|
+
from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
|
|
24
|
+
|
|
25
|
+
assert _sanitise_filename("project/api/init") == "project_api_init"
|
|
26
|
+
|
|
27
|
+
def test_backslash(self):
|
|
28
|
+
from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
|
|
29
|
+
|
|
30
|
+
assert _sanitise_filename("project\\api\\init") == "project_api_init"
|
|
31
|
+
|
|
32
|
+
def test_no_separators(self):
|
|
33
|
+
from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
|
|
34
|
+
|
|
35
|
+
assert _sanitise_filename("simple_name") == "simple_name"
|
|
36
|
+
|
|
37
|
+
def test_mixed_separators(self):
|
|
38
|
+
from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
|
|
39
|
+
|
|
40
|
+
assert _sanitise_filename("a/b\\c") == "a_b_c"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestRenderFuncDetail:
|
|
44
|
+
def test_basic_rendering(self):
|
|
45
|
+
from code_graph_builder.mcp.api_doc_generator import _render_func_detail
|
|
46
|
+
|
|
47
|
+
func = {
|
|
48
|
+
"qn": "project.api.init",
|
|
49
|
+
"name": "init",
|
|
50
|
+
"signature": "int init(void)",
|
|
51
|
+
"return_type": "int",
|
|
52
|
+
"visibility": "public",
|
|
53
|
+
"path": "src/api.c",
|
|
54
|
+
"start_line": 10,
|
|
55
|
+
"end_line": 20,
|
|
56
|
+
"module_qn": "project.api",
|
|
57
|
+
"docstring": "Initialize the API subsystem.",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
content = _render_func_detail(func, callers=[], callees=[])
|
|
61
|
+
|
|
62
|
+
assert "# init" in content
|
|
63
|
+
assert "`int init(void)`" in content
|
|
64
|
+
assert "`int`" in content
|
|
65
|
+
assert "public" in content
|
|
66
|
+
assert "Initialize the API subsystem." in content
|
|
67
|
+
assert "*(无调用者)*" in content
|
|
68
|
+
|
|
69
|
+
def test_with_callers_and_callees(self):
|
|
70
|
+
from code_graph_builder.mcp.api_doc_generator import _render_func_detail
|
|
71
|
+
|
|
72
|
+
func = {
|
|
73
|
+
"qn": "mod.foo",
|
|
74
|
+
"name": "foo",
|
|
75
|
+
"signature": "void foo()",
|
|
76
|
+
"return_type": None,
|
|
77
|
+
"visibility": "static",
|
|
78
|
+
"path": "src/mod.c",
|
|
79
|
+
"start_line": 5,
|
|
80
|
+
"end_line": 15,
|
|
81
|
+
"module_qn": "mod",
|
|
82
|
+
"docstring": None,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
callers = [{"qn": "mod.bar", "path": "src/mod.c", "start_line": 30}]
|
|
86
|
+
callees = [{"qn": "mod.baz", "path": "src/mod.c", "start_line": 50}]
|
|
87
|
+
|
|
88
|
+
content = _render_func_detail(func, callers=callers, callees=callees)
|
|
89
|
+
|
|
90
|
+
assert "被调用 (1)" in content
|
|
91
|
+
assert "mod.bar" in content
|
|
92
|
+
# No docstring section when docstring is None
|
|
93
|
+
assert "## 描述" not in content
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Unit tests for generate_api_docs pipeline
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestGenerateApiDocs:
|
|
102
|
+
def test_generates_files(self, tmp_path: Path):
|
|
103
|
+
from code_graph_builder.mcp.api_doc_generator import generate_api_docs
|
|
104
|
+
|
|
105
|
+
func_rows = [
|
|
106
|
+
{
|
|
107
|
+
"result": [
|
|
108
|
+
"mymod", # module_qn
|
|
109
|
+
"src/mymod.c", # module_path
|
|
110
|
+
"mymod.do_stuff", # qn
|
|
111
|
+
"do_stuff", # name
|
|
112
|
+
"int do_stuff(int x)", # signature
|
|
113
|
+
"int", # return_type
|
|
114
|
+
"public", # visibility
|
|
115
|
+
"x: int", # parameters
|
|
116
|
+
"Does stuff.", # docstring
|
|
117
|
+
1, # start_line
|
|
118
|
+
10, # end_line
|
|
119
|
+
"src/mymod.c", # path
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
result = generate_api_docs(
|
|
125
|
+
func_rows=func_rows,
|
|
126
|
+
type_rows=[],
|
|
127
|
+
call_rows=[],
|
|
128
|
+
output_dir=tmp_path,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
assert result["module_count"] == 1
|
|
132
|
+
assert result["func_count"] == 1
|
|
133
|
+
assert result["type_count"] == 0
|
|
134
|
+
|
|
135
|
+
# Check generated files
|
|
136
|
+
assert (tmp_path / "api_docs" / "index.md").exists()
|
|
137
|
+
assert (tmp_path / "api_docs" / "modules" / "mymod.md").exists()
|
|
138
|
+
assert (tmp_path / "api_docs" / "funcs" / "mymod.do_stuff.md").exists()
|
|
139
|
+
|
|
140
|
+
# Check L3 content
|
|
141
|
+
func_doc = (tmp_path / "api_docs" / "funcs" / "mymod.do_stuff.md").read_text()
|
|
142
|
+
assert "# do_stuff" in func_doc
|
|
143
|
+
assert "Does stuff." in func_doc
|
|
144
|
+
|
|
145
|
+
def test_call_graph_wiring(self, tmp_path: Path):
|
|
146
|
+
from code_graph_builder.mcp.api_doc_generator import generate_api_docs
|
|
147
|
+
|
|
148
|
+
func_rows = [
|
|
149
|
+
{
|
|
150
|
+
"result": [
|
|
151
|
+
"m", "m.c", "m.caller", "caller", "void caller()", None,
|
|
152
|
+
"public", "", None, 1, 5, "m.c",
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"result": [
|
|
157
|
+
"m", "m.c", "m.callee", "callee", "void callee()", None,
|
|
158
|
+
"static", "", None, 10, 15, "m.c",
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
call_rows = [
|
|
164
|
+
{"result": ["m.caller", "m.callee", "m.c", 3]},
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
generate_api_docs(
|
|
168
|
+
func_rows=func_rows,
|
|
169
|
+
type_rows=[],
|
|
170
|
+
call_rows=call_rows,
|
|
171
|
+
output_dir=tmp_path,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
caller_doc = (tmp_path / "api_docs" / "funcs" / "m.caller.md").read_text()
|
|
175
|
+
assert "callee" in caller_doc
|
|
176
|
+
|
|
177
|
+
callee_doc = (tmp_path / "api_docs" / "funcs" / "m.callee.md").read_text()
|
|
178
|
+
assert "caller" in callee_doc
|
|
179
|
+
assert "被调用 (1)" in callee_doc
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# Integration-style tests for the find_api aggregation logic
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestFindApiAggregation:
|
|
188
|
+
"""Test the core aggregation logic shared by cmd_api_find and _handle_find_api.
|
|
189
|
+
|
|
190
|
+
These tests exercise the filename-matching and doc-attachment logic
|
|
191
|
+
without requiring a live database or embeddings model.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def test_doc_attachment_when_file_exists(self, tmp_path: Path):
|
|
195
|
+
"""When a matching API doc file exists, its content is attached."""
|
|
196
|
+
funcs_dir = tmp_path / "api_docs" / "funcs"
|
|
197
|
+
funcs_dir.mkdir(parents=True)
|
|
198
|
+
|
|
199
|
+
doc_content = "# mymod.do_stuff\n\n- **Signature**: `int do_stuff(int x)`\n"
|
|
200
|
+
(funcs_dir / "mymod.do_stuff.md").write_text(doc_content)
|
|
201
|
+
|
|
202
|
+
# Simulate the attachment logic from cmd_api_find / _handle_find_api
|
|
203
|
+
qn = "mymod.do_stuff"
|
|
204
|
+
safe_qn = qn.replace("/", "_").replace("\\", "_")
|
|
205
|
+
doc_file = funcs_dir / f"{safe_qn}.md"
|
|
206
|
+
|
|
207
|
+
assert doc_file.exists()
|
|
208
|
+
assert doc_file.read_text() == doc_content
|
|
209
|
+
|
|
210
|
+
def test_doc_attachment_when_file_missing(self, tmp_path: Path):
|
|
211
|
+
"""When no API doc exists for a result, api_doc should be None."""
|
|
212
|
+
funcs_dir = tmp_path / "api_docs" / "funcs"
|
|
213
|
+
funcs_dir.mkdir(parents=True)
|
|
214
|
+
|
|
215
|
+
qn = "nonexistent.function"
|
|
216
|
+
safe_qn = qn.replace("/", "_").replace("\\", "_")
|
|
217
|
+
doc_file = funcs_dir / f"{safe_qn}.md"
|
|
218
|
+
|
|
219
|
+
assert not doc_file.exists()
|
|
220
|
+
|
|
221
|
+
def test_sanitise_slash_in_qn(self, tmp_path: Path):
|
|
222
|
+
"""Qualified names with slashes are sanitised to underscores for lookup."""
|
|
223
|
+
funcs_dir = tmp_path / "api_docs" / "funcs"
|
|
224
|
+
funcs_dir.mkdir(parents=True)
|
|
225
|
+
|
|
226
|
+
(funcs_dir / "path_to_func.md").write_text("doc")
|
|
227
|
+
|
|
228
|
+
qn = "path/to/func"
|
|
229
|
+
safe_qn = qn.replace("/", "_").replace("\\", "_")
|
|
230
|
+
doc_file = funcs_dir / f"{safe_qn}.md"
|
|
231
|
+
assert doc_file.exists()
|