auto-coder 0.1.399__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/METADATA +1 -1
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/RECORD +71 -35
- autocoder/agent/agentic_filter.py +1 -1
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +1 -1
- autocoder/auto_coder_runner.py +121 -26
- autocoder/chat_auto_coder.py +81 -22
- autocoder/commands/auto_command.py +1 -1
- autocoder/common/__init__.py +2 -2
- autocoder/common/ac_style_command_parser/parser.py +27 -12
- autocoder/common/auto_coder_lang.py +78 -0
- autocoder/common/command_completer_v2.py +1 -1
- autocoder/common/file_monitor/test_file_monitor.py +307 -0
- autocoder/common/git_utils.py +7 -2
- autocoder/common/pruner/__init__.py +0 -0
- autocoder/common/pruner/agentic_conversation_pruner.py +197 -0
- autocoder/common/pruner/context_pruner.py +574 -0
- autocoder/common/pruner/conversation_pruner.py +132 -0
- autocoder/common/pruner/test_agentic_conversation_pruner.py +342 -0
- autocoder/common/pruner/test_context_pruner.py +546 -0
- autocoder/common/pull_requests/__init__.py +256 -0
- autocoder/common/pull_requests/base_provider.py +191 -0
- autocoder/common/pull_requests/config.py +66 -0
- autocoder/common/pull_requests/example.py +1 -0
- autocoder/common/pull_requests/exceptions.py +46 -0
- autocoder/common/pull_requests/manager.py +201 -0
- autocoder/common/pull_requests/models.py +164 -0
- autocoder/common/pull_requests/providers/__init__.py +23 -0
- autocoder/common/pull_requests/providers/gitcode_provider.py +19 -0
- autocoder/common/pull_requests/providers/gitee_provider.py +20 -0
- autocoder/common/pull_requests/providers/github_provider.py +214 -0
- autocoder/common/pull_requests/providers/gitlab_provider.py +29 -0
- autocoder/common/pull_requests/test_module.py +1 -0
- autocoder/common/pull_requests/utils.py +344 -0
- autocoder/common/tokens/__init__.py +77 -0
- autocoder/common/tokens/counter.py +231 -0
- autocoder/common/tokens/file_detector.py +105 -0
- autocoder/common/tokens/filters.py +111 -0
- autocoder/common/tokens/models.py +28 -0
- autocoder/common/v2/agent/agentic_edit.py +538 -590
- autocoder/common/v2/agent/agentic_edit_tools/__init__.py +8 -1
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_read_tool_resolver.py +40 -0
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +43 -0
- autocoder/common/v2/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +8 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +1 -1
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +1 -1
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +33 -88
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +8 -8
- autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +118 -0
- autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +324 -0
- autocoder/common/v2/agent/agentic_edit_types.py +47 -4
- autocoder/common/v2/agent/runner/__init__.py +31 -0
- autocoder/common/v2/agent/runner/base_runner.py +106 -0
- autocoder/common/v2/agent/runner/event_runner.py +216 -0
- autocoder/common/v2/agent/runner/sdk_runner.py +40 -0
- autocoder/common/v2/agent/runner/terminal_runner.py +283 -0
- autocoder/common/v2/agent/runner/tool_display.py +191 -0
- autocoder/index/entry.py +1 -1
- autocoder/plugins/token_helper_plugin.py +107 -7
- autocoder/run_context.py +9 -0
- autocoder/sdk/__init__.py +114 -81
- autocoder/sdk/cli/handlers.py +2 -1
- autocoder/sdk/cli/main.py +9 -2
- autocoder/sdk/cli/options.py +4 -3
- autocoder/sdk/core/auto_coder_core.py +7 -152
- autocoder/sdk/core/bridge.py +5 -4
- autocoder/sdk/models/options.py +8 -6
- autocoder/version.py +1 -1
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import tempfile
|
|
3
|
+
import shutil
|
|
4
|
+
import os
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
from autocoder.common.pruner.context_pruner import PruneContext
|
|
7
|
+
from autocoder.common import AutoCoderArgs, SourceCode
|
|
8
|
+
from autocoder.sdk import get_llm, init_project_if_required
|
|
9
|
+
from autocoder.common.tokens import count_string_tokens
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPruneContextExtractStrategy:
|
|
13
|
+
"""Test suite for PruneContext extract strategy"""
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def temp_test_dir(self):
|
|
17
|
+
"""提供一个临时的、测试后自动清理的目录"""
|
|
18
|
+
# 保存原始工作目录
|
|
19
|
+
original_cwd = os.getcwd()
|
|
20
|
+
temp_dir = tempfile.mkdtemp()
|
|
21
|
+
try:
|
|
22
|
+
yield temp_dir
|
|
23
|
+
finally:
|
|
24
|
+
# 确保恢复到原始目录,即使出现异常
|
|
25
|
+
try:
|
|
26
|
+
os.chdir(original_cwd)
|
|
27
|
+
except OSError:
|
|
28
|
+
# 如果原始目录也不存在,则切换到用户主目录
|
|
29
|
+
os.chdir(os.path.expanduser("~"))
|
|
30
|
+
# 删除临时目录
|
|
31
|
+
if os.path.exists(temp_dir):
|
|
32
|
+
shutil.rmtree(temp_dir)
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def mock_args(self):
|
|
36
|
+
"""Create mock AutoCoderArgs for testing"""
|
|
37
|
+
return AutoCoderArgs(
|
|
38
|
+
source_dir=".",
|
|
39
|
+
context_prune=True,
|
|
40
|
+
context_prune_strategy="extract",
|
|
41
|
+
conversation_prune_safe_zone_tokens=30, # 这个不对context_prunner 生效
|
|
42
|
+
context_prune_sliding_window_size=10,
|
|
43
|
+
context_prune_sliding_window_overlap=2,
|
|
44
|
+
query="如何实现加法和减法运算?"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def real_llm(self):
|
|
49
|
+
"""创建真实的LLM对象"""
|
|
50
|
+
llm = get_llm("v3_chat", product_mode="lite")
|
|
51
|
+
return llm
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def pruner(self, mock_args, real_llm):
|
|
55
|
+
"""Create PruneContext instance for testing"""
|
|
56
|
+
# 对 context_prunner 生效的是 max_tokens这里
|
|
57
|
+
return PruneContext(max_tokens=60, args=mock_args, llm=real_llm)
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def verbose_pruner(self, mock_args, real_llm):
|
|
61
|
+
"""Create PruneContext instance with verbose=True for testing"""
|
|
62
|
+
return PruneContext(max_tokens=60, args=mock_args, llm=real_llm, verbose=True)
|
|
63
|
+
|
|
64
|
+
@pytest.fixture
|
|
65
|
+
def sample_file_sources(self, temp_test_dir):
|
|
66
|
+
"""Sample file sources for testing
|
|
67
|
+
Creates a simulated project structure in the temporary directory
|
|
68
|
+
"""
|
|
69
|
+
# 创建项目结构
|
|
70
|
+
src_dir = os.path.join(temp_test_dir, "src")
|
|
71
|
+
utils_dir = os.path.join(src_dir, "utils")
|
|
72
|
+
os.makedirs(utils_dir, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# 创建 __init__.py 文件使其成为有效的 Python 包
|
|
75
|
+
with open(os.path.join(src_dir, "__init__.py"), "w") as f:
|
|
76
|
+
f.write("# src package")
|
|
77
|
+
with open(os.path.join(utils_dir, "__init__.py"), "w") as f:
|
|
78
|
+
f.write("# utils package")
|
|
79
|
+
|
|
80
|
+
# 创建数学工具模块
|
|
81
|
+
math_utils_content = '''def add(a, b):
|
|
82
|
+
"""加法函数"""
|
|
83
|
+
return a + b
|
|
84
|
+
|
|
85
|
+
def subtract(a, b):
|
|
86
|
+
"""减法函数"""
|
|
87
|
+
return a - b
|
|
88
|
+
|
|
89
|
+
def multiply(a, b):
|
|
90
|
+
"""乘法函数"""
|
|
91
|
+
return a * b
|
|
92
|
+
|
|
93
|
+
def divide(a, b):
|
|
94
|
+
"""除法函数"""
|
|
95
|
+
if b == 0:
|
|
96
|
+
raise ValueError("Cannot divide by zero")
|
|
97
|
+
return a / b
|
|
98
|
+
'''
|
|
99
|
+
math_utils_path = os.path.join(utils_dir, "math_utils.py")
|
|
100
|
+
with open(math_utils_path, "w") as f:
|
|
101
|
+
f.write(math_utils_content)
|
|
102
|
+
|
|
103
|
+
# 创建字符串工具模块
|
|
104
|
+
string_utils_content = '''def format_string(s):
|
|
105
|
+
"""格式化字符串"""
|
|
106
|
+
return s.strip().lower()
|
|
107
|
+
|
|
108
|
+
def reverse_string(s):
|
|
109
|
+
"""反转字符串"""
|
|
110
|
+
return s[::-1]
|
|
111
|
+
|
|
112
|
+
def count_characters(s):
|
|
113
|
+
"""计算字符数"""
|
|
114
|
+
return len(s)
|
|
115
|
+
'''
|
|
116
|
+
string_utils_path = os.path.join(utils_dir, "string_utils.py")
|
|
117
|
+
with open(string_utils_path, "w") as f:
|
|
118
|
+
f.write(string_utils_content)
|
|
119
|
+
|
|
120
|
+
# 创建主程序文件
|
|
121
|
+
main_content = '''from utils.math_utils import add, subtract
|
|
122
|
+
from utils.string_utils import format_string
|
|
123
|
+
|
|
124
|
+
def main():
|
|
125
|
+
print("计算结果:", add(5, 3))
|
|
126
|
+
print("格式化结果:", format_string(" Hello World "))
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
130
|
+
'''
|
|
131
|
+
main_path = os.path.join(src_dir, "main.py")
|
|
132
|
+
with open(main_path, "w") as f:
|
|
133
|
+
f.write(main_content)
|
|
134
|
+
|
|
135
|
+
# 创建 README 文件
|
|
136
|
+
readme_content = '''# 测试项目
|
|
137
|
+
|
|
138
|
+
这是一个用于测试的模拟项目结构。
|
|
139
|
+
|
|
140
|
+
## 功能
|
|
141
|
+
|
|
142
|
+
- 数学运算
|
|
143
|
+
- 字符串处理
|
|
144
|
+
'''
|
|
145
|
+
readme_path = os.path.join(temp_test_dir, "README.md")
|
|
146
|
+
with open(readme_path, "w") as f:
|
|
147
|
+
f.write(readme_content)
|
|
148
|
+
|
|
149
|
+
# 初始化该项目
|
|
150
|
+
# 保存当前工作目录
|
|
151
|
+
original_cwd = os.getcwd()
|
|
152
|
+
try:
|
|
153
|
+
os.chdir(temp_test_dir)
|
|
154
|
+
init_project_if_required(target_dir=temp_test_dir)
|
|
155
|
+
finally:
|
|
156
|
+
# 立即恢复工作目录,避免影响后续测试
|
|
157
|
+
os.chdir(original_cwd)
|
|
158
|
+
|
|
159
|
+
# 返回与原来相同的 SourceCode 对象列表,但使用相对路径作为 module_name
|
|
160
|
+
v = [
|
|
161
|
+
SourceCode(
|
|
162
|
+
module_name="src/utils/math_utils.py",
|
|
163
|
+
source_code=math_utils_content,
|
|
164
|
+
tokens=count_string_tokens(math_utils_content)
|
|
165
|
+
),
|
|
166
|
+
SourceCode(
|
|
167
|
+
module_name="src/utils/string_utils.py",
|
|
168
|
+
source_code=string_utils_content,
|
|
169
|
+
tokens=count_string_tokens(string_utils_content)
|
|
170
|
+
),
|
|
171
|
+
SourceCode(
|
|
172
|
+
module_name="src/main.py",
|
|
173
|
+
source_code=main_content,
|
|
174
|
+
tokens=count_string_tokens(main_content)
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
return v
|
|
179
|
+
|
|
180
|
+
@pytest.fixture
|
|
181
|
+
def sample_conversations(self):
|
|
182
|
+
"""Sample conversations for testing"""
|
|
183
|
+
return [
|
|
184
|
+
{"role": "user", "content": "如何实现加法和减法运算?"},
|
|
185
|
+
{"role": "assistant", "content": "我来帮你实现加法和减法运算。"}
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
def test_extract_strategy_basic(self, pruner, sample_file_sources, sample_conversations):
|
|
189
|
+
"""测试extract策略的基本功能"""
|
|
190
|
+
|
|
191
|
+
# 计算输入的总token数
|
|
192
|
+
original_total_tokens = sum(source.tokens for source in sample_file_sources)
|
|
193
|
+
print(f"原始总token数: {original_total_tokens}")
|
|
194
|
+
|
|
195
|
+
result = pruner.handle_overflow(
|
|
196
|
+
file_sources=sample_file_sources,
|
|
197
|
+
conversations=sample_conversations,
|
|
198
|
+
strategy="extract"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# 验证结果基本结构
|
|
202
|
+
assert isinstance(result, list), "应该返回文件列表"
|
|
203
|
+
print(f"返回文件数量: {len(result)}")
|
|
204
|
+
|
|
205
|
+
# 验证返回的是SourceCode对象
|
|
206
|
+
for item in result:
|
|
207
|
+
assert isinstance(item, SourceCode), "返回的应该是SourceCode对象"
|
|
208
|
+
assert hasattr(item, 'module_name'), "SourceCode应该有module_name属性"
|
|
209
|
+
assert hasattr(item, 'source_code'), "SourceCode应该有source_code属性"
|
|
210
|
+
|
|
211
|
+
# 如果有结果,验证token压缩效果
|
|
212
|
+
if len(result) > 0:
|
|
213
|
+
# 计算输出的总token数
|
|
214
|
+
result_total_tokens = sum(item.tokens for item in result)
|
|
215
|
+
print(f"处理后总token数: {result_total_tokens}")
|
|
216
|
+
|
|
217
|
+
# 验证token数确实减少了
|
|
218
|
+
assert result_total_tokens < original_total_tokens, f"Token数应该减少,原始: {original_total_tokens}, 处理后: {result_total_tokens}"
|
|
219
|
+
|
|
220
|
+
# 计算压缩率
|
|
221
|
+
compression_rate = (original_total_tokens - result_total_tokens) / original_total_tokens * 100
|
|
222
|
+
print(f"Token压缩率: {compression_rate:.1f}%")
|
|
223
|
+
assert compression_rate > 0, "应该有有效的压缩率"
|
|
224
|
+
|
|
225
|
+
# 验证与查询相关的函数是否在结果中(用户查询:"如何实现加法和减法运算?")
|
|
226
|
+
combined_content = "\n".join(item.source_code for item in result)
|
|
227
|
+
print(f"合并后的内容长度: {len(combined_content)} 字符")
|
|
228
|
+
|
|
229
|
+
# 检查加法和减法相关的函数是否存在
|
|
230
|
+
has_add_function = "def add(" in combined_content or "add(" in combined_content
|
|
231
|
+
has_subtract_function = "def subtract(" in combined_content or "subtract(" in combined_content
|
|
232
|
+
|
|
233
|
+
print(f"包含add函数: {has_add_function}")
|
|
234
|
+
print(f"包含subtract函数: {has_subtract_function}")
|
|
235
|
+
|
|
236
|
+
# 至少应该包含其中一个相关函数
|
|
237
|
+
assert has_add_function or has_subtract_function, "裁剪后的结果应该包含与查询相关的加法或减法函数"
|
|
238
|
+
|
|
239
|
+
# 验证math_utils.py是否在结果中(因为它包含相关函数)
|
|
240
|
+
math_utils_files = [item for item in result if "math_utils.py" in item.module_name]
|
|
241
|
+
if math_utils_files:
|
|
242
|
+
math_utils_content = math_utils_files[0].source_code
|
|
243
|
+
print(f"math_utils.py处理后内容:\n{math_utils_content}")
|
|
244
|
+
|
|
245
|
+
# 验证包含Snippets标记(说明经过了代码片段抽取)
|
|
246
|
+
assert "Snippets:" in math_utils_content, "math_utils.py应该包含代码片段抽取标记"
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
print("⚠️ Extract策略返回空结果(这可能发生在LLM超时或其他异常情况下)")
|
|
250
|
+
|
|
251
|
+
def test_sliding_window_split(self, pruner):
|
|
252
|
+
"""测试滑动窗口分割功能"""
|
|
253
|
+
# 创建一个较长的内容用于测试(20行)
|
|
254
|
+
content = "\n".join([f"line {i}: some content here" for i in range(1, 21)])
|
|
255
|
+
content_lines = content.split('\n')
|
|
256
|
+
total_lines = len(content_lines)
|
|
257
|
+
|
|
258
|
+
print(f"测试内容总行数: {total_lines}")
|
|
259
|
+
|
|
260
|
+
# 测试不同的滑动窗口配置
|
|
261
|
+
test_cases = [
|
|
262
|
+
{
|
|
263
|
+
"window_size": 5,
|
|
264
|
+
"overlap": 2,
|
|
265
|
+
"expected_chunks": 7, # 基于实际运行结果
|
|
266
|
+
"description": "标准滑动窗口配置"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"window_size": 3,
|
|
270
|
+
"overlap": 1,
|
|
271
|
+
"expected_chunks": 10, # 基于实际运行结果
|
|
272
|
+
"description": "小窗口高重叠配置"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"window_size": 10,
|
|
276
|
+
"overlap": 3,
|
|
277
|
+
"expected_chunks": 3, # 基于实际运行结果
|
|
278
|
+
"description": "大窗口中等重叠配置"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"window_size": 7,
|
|
282
|
+
"overlap": 0,
|
|
283
|
+
"expected_chunks": 3, # 基于实际运行结果
|
|
284
|
+
"description": "无重叠配置"
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
for case in test_cases:
|
|
289
|
+
window_size = case["window_size"]
|
|
290
|
+
overlap = case["overlap"]
|
|
291
|
+
expected_chunks = case["expected_chunks"]
|
|
292
|
+
description = case["description"]
|
|
293
|
+
|
|
294
|
+
print(f"\n🔍 测试 {description}: 窗口={window_size}, 重叠={overlap}")
|
|
295
|
+
|
|
296
|
+
chunks = pruner._split_content_with_sliding_window(
|
|
297
|
+
content=content,
|
|
298
|
+
window_size=window_size,
|
|
299
|
+
overlap=overlap
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# 验证基本结构
|
|
303
|
+
assert isinstance(chunks, list), f"应该返回chunk列表 ({description})"
|
|
304
|
+
assert len(chunks) == expected_chunks, f"应该有 {expected_chunks} 个chunks,实际: {len(chunks)} ({description})"
|
|
305
|
+
|
|
306
|
+
# 验证chunk结构和内容
|
|
307
|
+
for i, chunk in enumerate(chunks):
|
|
308
|
+
assert isinstance(chunk, tuple), f"Chunk {i+1} 应该是元组 ({description})"
|
|
309
|
+
assert len(chunk) == 3, f"Chunk {i+1} 应该包含3个元素 ({description})"
|
|
310
|
+
|
|
311
|
+
start_line, end_line, chunk_content = chunk
|
|
312
|
+
assert isinstance(start_line, int), f"Chunk {i+1} 起始行号应该是整数 ({description})"
|
|
313
|
+
assert isinstance(end_line, int), f"Chunk {i+1} 结束行号应该是整数 ({description})"
|
|
314
|
+
assert isinstance(chunk_content, str), f"Chunk {i+1} 内容应该是字符串 ({description})"
|
|
315
|
+
assert start_line <= end_line, f"Chunk {i+1} 起始行号应该 <= 结束行号 ({description})"
|
|
316
|
+
|
|
317
|
+
# 验证行号范围合理
|
|
318
|
+
assert 1 <= start_line <= total_lines, f"Chunk {i+1} 起始行号应该在1-{total_lines}范围内 ({description})"
|
|
319
|
+
assert 1 <= end_line <= total_lines, f"Chunk {i+1} 结束行号应该在1-{total_lines}范围内 ({description})"
|
|
320
|
+
|
|
321
|
+
# 验证内容行数与行号范围一致
|
|
322
|
+
chunk_lines = chunk_content.split('\n')
|
|
323
|
+
expected_line_count = end_line - start_line + 1
|
|
324
|
+
# 注意:由于chunk_content中可能包含行号前缀,实际行数可能不同
|
|
325
|
+
# 我们主要验证内容不为空且合理
|
|
326
|
+
assert len(chunk_content) > 0, f"Chunk {i+1} 内容不应为空 ({description})"
|
|
327
|
+
|
|
328
|
+
print(f" Chunk {i+1}: 行 {start_line}-{end_line} ({len(chunk_lines)} 行)")
|
|
329
|
+
|
|
330
|
+
# 验证重叠逻辑
|
|
331
|
+
if overlap > 0 and len(chunks) > 1:
|
|
332
|
+
for i in range(len(chunks) - 1):
|
|
333
|
+
current_start, current_end, _ = chunks[i]
|
|
334
|
+
next_start, next_end, _ = chunks[i + 1]
|
|
335
|
+
|
|
336
|
+
# 验证有重叠
|
|
337
|
+
if i < len(chunks) - 2: # 不是最后一个chunk对
|
|
338
|
+
overlap_lines = current_end - next_start + 1
|
|
339
|
+
assert overlap_lines >= overlap, f"Chunk {i+1} 和 {i+2} 重叠行数应该 >= {overlap},实际: {overlap_lines} ({description})"
|
|
340
|
+
|
|
341
|
+
# 验证覆盖完整性(所有行都被覆盖)
|
|
342
|
+
covered_lines = set()
|
|
343
|
+
for start_line, end_line, _ in chunks:
|
|
344
|
+
for line_num in range(start_line, end_line + 1):
|
|
345
|
+
covered_lines.add(line_num)
|
|
346
|
+
|
|
347
|
+
expected_lines = set(range(1, total_lines + 1))
|
|
348
|
+
assert covered_lines == expected_lines, f"应该覆盖所有行1-{total_lines} ({description})"
|
|
349
|
+
|
|
350
|
+
print(f" ✅ 验证通过: {len(chunks)} 个chunks,覆盖所有 {total_lines} 行")
|
|
351
|
+
|
|
352
|
+
# 追加基于math_utils_content的真实代码测试
|
|
353
|
+
print(f"\n🧮 测试真实代码内容 - math_utils.py")
|
|
354
|
+
math_utils_content = '''def add(a, b):
|
|
355
|
+
"""加法函数"""
|
|
356
|
+
return a + b
|
|
357
|
+
|
|
358
|
+
def subtract(a, b):
|
|
359
|
+
"""减法函数"""
|
|
360
|
+
return a - b
|
|
361
|
+
|
|
362
|
+
def multiply(a, b):
|
|
363
|
+
"""乘法函数"""
|
|
364
|
+
return a * b
|
|
365
|
+
|
|
366
|
+
def divide(a, b):
|
|
367
|
+
"""除法函数"""
|
|
368
|
+
if b == 0:
|
|
369
|
+
raise ValueError("Cannot divide by zero")
|
|
370
|
+
return a / b
|
|
371
|
+
'''
|
|
372
|
+
|
|
373
|
+
math_content_lines = math_utils_content.split('\n')
|
|
374
|
+
math_total_lines = len([line for line in math_content_lines if line.strip()]) # 只计算非空行
|
|
375
|
+
print(f"math_utils.py 总行数: {len(math_content_lines)} (非空行: {math_total_lines})")
|
|
376
|
+
|
|
377
|
+
# 测试适合代码的窗口配置
|
|
378
|
+
math_test_cases = [
|
|
379
|
+
{
|
|
380
|
+
"window_size": 4,
|
|
381
|
+
"overlap": 1,
|
|
382
|
+
"description": "小窗口配置适合函数分割"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"window_size": 6,
|
|
386
|
+
"overlap": 2,
|
|
387
|
+
"description": "中等窗口配置包含完整函数"
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
for case in math_test_cases:
|
|
392
|
+
window_size = case["window_size"]
|
|
393
|
+
overlap = case["overlap"]
|
|
394
|
+
description = case["description"]
|
|
395
|
+
|
|
396
|
+
print(f"\n🔍 测试 {description}: 窗口={window_size}, 重叠={overlap}")
|
|
397
|
+
|
|
398
|
+
chunks = pruner._split_content_with_sliding_window(
|
|
399
|
+
content=math_utils_content,
|
|
400
|
+
window_size=window_size,
|
|
401
|
+
overlap=overlap
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
print(f" 生成 {len(chunks)} 个chunks:")
|
|
405
|
+
|
|
406
|
+
# 验证基本结构
|
|
407
|
+
assert isinstance(chunks, list), f"应该返回chunk列表 ({description})"
|
|
408
|
+
assert len(chunks) > 0, f"应该至少有一个chunk ({description})"
|
|
409
|
+
|
|
410
|
+
# 分析每个chunk的内容
|
|
411
|
+
function_keywords = ["def add(", "def subtract(", "def multiply(", "def divide("]
|
|
412
|
+
|
|
413
|
+
for i, chunk in enumerate(chunks):
|
|
414
|
+
start_line, end_line, chunk_content = chunk
|
|
415
|
+
print(f" Chunk {i+1}: 行 {start_line}-{end_line}")
|
|
416
|
+
|
|
417
|
+
# 检查这个chunk包含哪些函数定义
|
|
418
|
+
found_functions = []
|
|
419
|
+
for keyword in function_keywords:
|
|
420
|
+
if keyword in chunk_content:
|
|
421
|
+
func_name = keyword[4:-1] # 提取函数名 (去掉 "def " 和 "(")
|
|
422
|
+
found_functions.append(func_name)
|
|
423
|
+
|
|
424
|
+
if found_functions:
|
|
425
|
+
print(f" 包含函数: {', '.join(found_functions)}")
|
|
426
|
+
else:
|
|
427
|
+
print(f" 包含: 函数体或注释部分")
|
|
428
|
+
|
|
429
|
+
# 验证基本结构
|
|
430
|
+
assert isinstance(chunk, tuple), f"Chunk {i+1} 应该是元组 ({description})"
|
|
431
|
+
assert len(chunk) == 3, f"Chunk {i+1} 应该包含3个元素 ({description})"
|
|
432
|
+
assert isinstance(start_line, int), f"Chunk {i+1} 起始行号应该是整数 ({description})"
|
|
433
|
+
assert isinstance(end_line, int), f"Chunk {i+1} 结束行号应该是整数 ({description})"
|
|
434
|
+
assert isinstance(chunk_content, str), f"Chunk {i+1} 内容应该是字符串 ({description})"
|
|
435
|
+
assert len(chunk_content.strip()) > 0, f"Chunk {i+1} 内容不应为空 ({description})"
|
|
436
|
+
|
|
437
|
+
# 验证所有函数定义都被覆盖
|
|
438
|
+
all_chunk_content = "\n".join(chunk[2] for chunk in chunks)
|
|
439
|
+
for keyword in function_keywords:
|
|
440
|
+
assert keyword in all_chunk_content, f"应该覆盖函数定义: {keyword} ({description})"
|
|
441
|
+
|
|
442
|
+
print(f" ✅ 验证通过: 所有函数定义都被覆盖")
|
|
443
|
+
|
|
444
|
+
def test_merge_overlapping_snippets(self, pruner):
|
|
445
|
+
"""测试重叠片段合并功能"""
|
|
446
|
+
# 测试重叠片段
|
|
447
|
+
snippets = [
|
|
448
|
+
{"start_line": 1, "end_line": 5},
|
|
449
|
+
{"start_line": 4, "end_line": 8},
|
|
450
|
+
{"start_line": 10, "end_line": 15}
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
merged = pruner._merge_overlapping_snippets(snippets)
|
|
454
|
+
|
|
455
|
+
# 验证结果
|
|
456
|
+
assert isinstance(merged, list), "应该返回片段列表"
|
|
457
|
+
assert len(merged) == 2, "应该合并为2个片段"
|
|
458
|
+
|
|
459
|
+
# 验证合并结果
|
|
460
|
+
assert merged[0]["start_line"] == 1, "第一个片段起始行应该是1"
|
|
461
|
+
assert merged[0]["end_line"] == 8, "第一个片段结束行应该是8"
|
|
462
|
+
assert merged[1]["start_line"] == 10, "第二个片段起始行应该是10"
|
|
463
|
+
assert merged[1]["end_line"] == 15, "第二个片段结束行应该是15"
|
|
464
|
+
|
|
465
|
+
def test_build_snippet_content(self, pruner):
|
|
466
|
+
"""测试构建片段内容功能"""
|
|
467
|
+
full_content = """def add(a, b):
|
|
468
|
+
return a + b
|
|
469
|
+
|
|
470
|
+
def subtract(a, b):
|
|
471
|
+
return a - b
|
|
472
|
+
|
|
473
|
+
def multiply(a, b):
|
|
474
|
+
return a * b"""
|
|
475
|
+
|
|
476
|
+
snippets = [
|
|
477
|
+
{"start_line": 1, "end_line": 2},
|
|
478
|
+
{"start_line": 4, "end_line": 5}
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
result = pruner._build_snippet_content(
|
|
482
|
+
"test.py", full_content, snippets)
|
|
483
|
+
|
|
484
|
+
# 验证结果
|
|
485
|
+
assert isinstance(result, str), "应该返回字符串"
|
|
486
|
+
assert "Snippets:" in result, "应该包含Snippets标题"
|
|
487
|
+
assert "def add(a, b):" in result, "应该包含add函数"
|
|
488
|
+
assert "def subtract(a, b):" in result, "应该包含subtract函数"
|
|
489
|
+
|
|
490
|
+
def test_invalid_strategy(self, pruner, sample_file_sources, sample_conversations):
|
|
491
|
+
"""测试无效策略处理"""
|
|
492
|
+
with pytest.raises(ValueError) as exc_info:
|
|
493
|
+
pruner.handle_overflow(
|
|
494
|
+
file_sources=sample_file_sources,
|
|
495
|
+
conversations=sample_conversations,
|
|
496
|
+
strategy="invalid_strategy"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
assert "无效策略" in str(exc_info.value), "应该抛出无效策略错误"
|
|
500
|
+
|
|
501
|
+
def test_verbose_functionality(self, verbose_pruner, sample_file_sources, sample_conversations, capsys):
|
|
502
|
+
"""测试verbose参数的功能"""
|
|
503
|
+
# 使用verbose=True的pruner进行测试
|
|
504
|
+
result = verbose_pruner.handle_overflow(
|
|
505
|
+
file_sources=sample_file_sources,
|
|
506
|
+
conversations=sample_conversations,
|
|
507
|
+
strategy="extract"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# 捕获输出
|
|
511
|
+
captured = capsys.readouterr()
|
|
512
|
+
|
|
513
|
+
# 验证verbose输出包含预期的信息
|
|
514
|
+
assert "🚀 开始代码片段抽取处理" in captured.out, "应该包含开始处理的信息"
|
|
515
|
+
assert "📋 处理策略" in captured.out, "应该包含处理策略信息"
|
|
516
|
+
assert "🎯 代码片段抽取处理完成" in captured.out, "应该包含处理完成的信息"
|
|
517
|
+
assert "📊 处理结果统计" in captured.out, "应该包含结果统计信息"
|
|
518
|
+
|
|
519
|
+
# 验证结果仍然正确
|
|
520
|
+
assert isinstance(result, list), "应该返回文件列表"
|
|
521
|
+
assert len(result) >= 0, "应该返回有效的结果列表"
|
|
522
|
+
|
|
523
|
+
def test_non_verbose_functionality(self, pruner, sample_file_sources, sample_conversations, capsys):
|
|
524
|
+
"""测试verbose=False时不输出详细信息"""
|
|
525
|
+
# 使用verbose=False的pruner进行测试
|
|
526
|
+
result = pruner.handle_overflow(
|
|
527
|
+
file_sources=sample_file_sources,
|
|
528
|
+
conversations=sample_conversations,
|
|
529
|
+
strategy="extract"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# 捕获输出
|
|
533
|
+
captured = capsys.readouterr()
|
|
534
|
+
|
|
535
|
+
# 验证不包含verbose特有的输出
|
|
536
|
+
assert "🚀 开始代码片段抽取处理" not in captured.out, "非verbose模式不应该包含详细处理信息"
|
|
537
|
+
assert "📋 处理策略" not in captured.out, "非verbose模式不应该包含处理策略信息"
|
|
538
|
+
assert "🎯 代码片段抽取处理完成" not in captured.out, "非verbose模式不应该包含处理完成的详细信息"
|
|
539
|
+
|
|
540
|
+
# 验证结果仍然正确
|
|
541
|
+
assert isinstance(result, list), "应该返回文件列表"
|
|
542
|
+
assert len(result) >= 0, "应该返回有效的结果列表"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
if __name__ == "__main__":
|
|
546
|
+
pytest.main([__file__, "-v"])
|