366 lines
13 KiB
Python
366 lines
13 KiB
Python
"""
|
|
文件选择与排除模式协同功能测试
|
|
|
|
测试场景:
|
|
1. 获取项目文件列表 - 无排除模式
|
|
2. 获取项目文件列表 - 带排除模式
|
|
3. ZIP 扫描 - 带排除模式
|
|
4. 仓库扫描 - 带排除模式
|
|
5. 排除模式与文件选择的协同
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
# 添加项目根目录到 Python 路径
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
try:
|
|
import pytest
|
|
HAS_PYTEST = True
|
|
except ImportError:
|
|
HAS_PYTEST = False
|
|
# 创建一个简单的 pytest.mark 模拟
|
|
class MockPytest:
|
|
class mark:
|
|
@staticmethod
|
|
def asyncio(func):
|
|
return func
|
|
pytest = MockPytest()
|
|
|
|
from app.services.scanner import should_exclude, is_text_file, EXCLUDE_PATTERNS
|
|
|
|
|
|
class TestShouldExclude:
|
|
"""测试 should_exclude 函数"""
|
|
|
|
def test_default_exclude_patterns(self):
|
|
"""测试默认排除模式"""
|
|
# 应该被排除的路径
|
|
assert should_exclude("node_modules/package.json") is True
|
|
assert should_exclude(".git/config") is True
|
|
assert should_exclude("dist/bundle.js") is True
|
|
assert should_exclude("build/output.js") is True
|
|
assert should_exclude("__pycache__/module.pyc") is True
|
|
assert should_exclude("vendor/lib.php") is True
|
|
|
|
def test_default_not_excluded(self):
|
|
"""测试不应该被排除的路径"""
|
|
assert should_exclude("src/main.py") is False
|
|
assert should_exclude("app/index.js") is False
|
|
assert should_exclude("lib/utils.ts") is False
|
|
|
|
def test_custom_exclude_patterns(self):
|
|
"""测试自定义排除模式"""
|
|
# 注意:当前实现使用简单的 'in' 匹配,不是 glob 模式
|
|
# 所以模式应该是路径片段,如 ".log", "temp/", ".bak"
|
|
custom_patterns = [".log", "temp/", ".bak"]
|
|
|
|
# 应该被排除(包含模式字符串)
|
|
assert should_exclude("app.log", custom_patterns) is True
|
|
assert should_exclude("temp/cache.txt", custom_patterns) is True
|
|
assert should_exclude("config.bak", custom_patterns) is True
|
|
|
|
# 不应该被排除
|
|
assert should_exclude("src/main.py", custom_patterns) is False
|
|
|
|
def test_combined_patterns(self):
|
|
"""测试默认模式和自定义模式组合"""
|
|
# 使用路径片段匹配
|
|
custom_patterns = [".test.js", "coverage/"]
|
|
|
|
# 默认模式排除
|
|
assert should_exclude("node_modules/lib.js", custom_patterns) is True
|
|
# 自定义模式排除
|
|
assert should_exclude("app.test.js", custom_patterns) is True
|
|
assert should_exclude("coverage/report.html", custom_patterns) is True
|
|
# 都不排除
|
|
assert should_exclude("src/app.js", custom_patterns) is False
|
|
|
|
|
|
class TestIsTextFile:
|
|
"""测试 is_text_file 函数"""
|
|
|
|
def test_supported_extensions(self):
|
|
"""测试支持的文件扩展名"""
|
|
supported = [
|
|
"main.js", "app.ts", "component.tsx", "page.jsx",
|
|
"script.py", "Main.java", "main.go", "lib.rs",
|
|
"app.cpp", "header.h", "Program.cs", "index.php",
|
|
"app.rb", "App.swift", "Main.kt", "query.sql",
|
|
"script.sh", "config.json", "config.yml", "config.yaml"
|
|
]
|
|
for filename in supported:
|
|
assert is_text_file(filename) is True, f"{filename} should be supported"
|
|
|
|
def test_unsupported_extensions(self):
|
|
"""测试不支持的文件扩展名"""
|
|
unsupported = [
|
|
"image.png", "photo.jpg", "doc.pdf", "archive.zip",
|
|
"binary.exe", "data.bin", "video.mp4", "audio.mp3"
|
|
]
|
|
for filename in unsupported:
|
|
assert is_text_file(filename) is False, f"{filename} should not be supported"
|
|
|
|
|
|
class TestExcludePatternsIntegration:
|
|
"""排除模式集成测试"""
|
|
|
|
def test_exclude_patterns_with_path_segments(self):
|
|
"""测试路径片段匹配"""
|
|
# 当前实现使用 'in' 匹配,所以使用路径片段
|
|
patterns = ["tests/", ".test.js"]
|
|
|
|
# 这些应该被排除
|
|
assert should_exclude("src/tests/unit.js", patterns) is True
|
|
assert should_exclude("app.test.js", patterns) is True
|
|
|
|
def test_empty_exclude_patterns(self):
|
|
"""测试空排除模式列表"""
|
|
# 空列表应该只使用默认模式
|
|
assert should_exclude("node_modules/lib.js", []) is True
|
|
assert should_exclude("src/main.py", []) is False
|
|
|
|
def test_none_exclude_patterns(self):
|
|
"""测试 None 排除模式"""
|
|
assert should_exclude("node_modules/lib.js", None) is True
|
|
assert should_exclude("src/main.py", None) is False
|
|
|
|
|
|
class TestFileSelectionWorkflow:
|
|
"""文件选择工作流测试"""
|
|
|
|
def create_test_zip(self, files: dict) -> str:
|
|
"""创建测试用的 ZIP 文件"""
|
|
temp_dir = tempfile.mkdtemp()
|
|
zip_path = os.path.join(temp_dir, "test.zip")
|
|
|
|
with zipfile.ZipFile(zip_path, 'w') as zf:
|
|
for filename, content in files.items():
|
|
zf.writestr(filename, content)
|
|
|
|
return zip_path
|
|
|
|
def test_zip_file_filtering(self):
|
|
"""测试 ZIP 文件过滤逻辑"""
|
|
# 模拟 ZIP 文件内容
|
|
files = {
|
|
"src/main.py": "print('hello')",
|
|
"src/utils.py": "def util(): pass",
|
|
"node_modules/lib.js": "module.exports = {}",
|
|
"dist/bundle.js": "var a = 1;",
|
|
".git/config": "[core]",
|
|
"tests/test_main.py": "def test(): pass",
|
|
"app.log": "log content",
|
|
"README.md": "# Readme",
|
|
}
|
|
|
|
zip_path = self.create_test_zip(files)
|
|
|
|
try:
|
|
# 模拟文件过滤逻辑
|
|
filtered_files = []
|
|
# 使用路径片段匹配(当前实现方式)
|
|
custom_exclude = [".log", ".md"]
|
|
|
|
with zipfile.ZipFile(zip_path, 'r') as zf:
|
|
for file_info in zf.infolist():
|
|
if not file_info.is_dir():
|
|
path = file_info.filename
|
|
if is_text_file(path) and not should_exclude(path, custom_exclude):
|
|
filtered_files.append(path)
|
|
|
|
# 验证过滤结果
|
|
assert "src/main.py" in filtered_files
|
|
assert "src/utils.py" in filtered_files
|
|
assert "tests/test_main.py" in filtered_files
|
|
|
|
# 这些应该被排除
|
|
assert "node_modules/lib.js" not in filtered_files # 默认排除
|
|
assert "dist/bundle.js" not in filtered_files # 默认排除
|
|
assert ".git/config" not in filtered_files # 默认排除
|
|
assert "app.log" not in filtered_files # 自定义排除 (.log)
|
|
assert "README.md" not in filtered_files # 自定义排除 (.md) + 不是代码文件
|
|
|
|
finally:
|
|
os.remove(zip_path)
|
|
os.rmdir(os.path.dirname(zip_path))
|
|
|
|
def test_file_selection_with_exclude(self):
|
|
"""测试文件选择与排除模式的协同"""
|
|
# 模拟从 API 返回的文件列表(已应用排除模式)
|
|
all_files = [
|
|
{"path": "src/main.py", "size": 100},
|
|
{"path": "src/utils.py", "size": 200},
|
|
{"path": "src/tests/test_main.py", "size": 150},
|
|
{"path": "lib/helper.py", "size": 80},
|
|
]
|
|
|
|
# 用户选择部分文件
|
|
selected_files = ["src/main.py", "src/utils.py"]
|
|
|
|
# 验证选择的文件都在可用列表中
|
|
available_paths = {f["path"] for f in all_files}
|
|
for selected in selected_files:
|
|
assert selected in available_paths
|
|
|
|
def test_exclude_patterns_change_clears_selection(self):
|
|
"""测试排除模式变化时应清空文件选择"""
|
|
# 模拟初始状态
|
|
initial_exclude = ["node_modules/**", ".git/**"]
|
|
selected_files = ["src/main.py", "src/utils.py"]
|
|
|
|
# 模拟排除模式变化
|
|
new_exclude = ["node_modules/**", ".git/**", "src/utils.py"]
|
|
|
|
# 当排除模式变化时,应该清空选择
|
|
# 因为 src/utils.py 现在被排除了
|
|
if initial_exclude != new_exclude:
|
|
# 前端逻辑:清空选择
|
|
selected_files = None
|
|
|
|
assert selected_files is None
|
|
|
|
|
|
class TestAPIEndpoints:
|
|
"""API 端点测试(模拟)"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_project_files_with_exclude(self):
|
|
"""测试获取项目文件 API 带排除模式"""
|
|
# 模拟请求参数
|
|
project_id = "test-project-id"
|
|
branch = "main"
|
|
exclude_patterns = json.dumps(["*.log", "temp/**"])
|
|
|
|
# 验证参数格式正确
|
|
parsed_patterns = json.loads(exclude_patterns)
|
|
assert isinstance(parsed_patterns, list)
|
|
assert "*.log" in parsed_patterns
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_request_with_exclude(self):
|
|
"""测试扫描请求带排除模式"""
|
|
scan_config = {
|
|
"file_paths": ["src/main.py", "src/utils.py"],
|
|
"exclude_patterns": ["*.test.js", "coverage/**"],
|
|
"full_scan": False,
|
|
"rule_set_id": None,
|
|
"prompt_template_id": None,
|
|
}
|
|
|
|
# 验证配置格式
|
|
assert "exclude_patterns" in scan_config
|
|
assert isinstance(scan_config["exclude_patterns"], list)
|
|
assert scan_config["full_scan"] is False
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""边界情况测试"""
|
|
|
|
def test_empty_file_list(self):
|
|
"""测试空文件列表"""
|
|
files = []
|
|
exclude_patterns = ["*.log"]
|
|
|
|
filtered = [f for f in files if not should_exclude(f, exclude_patterns)]
|
|
assert filtered == []
|
|
|
|
def test_all_files_excluded(self):
|
|
"""测试所有文件都被排除"""
|
|
files = ["node_modules/a.js", "dist/b.js", ".git/config"]
|
|
|
|
filtered = [f for f in files if not should_exclude(f)]
|
|
assert filtered == []
|
|
|
|
def test_special_characters_in_path(self):
|
|
"""测试路径中的特殊字符"""
|
|
paths = [
|
|
"src/file with spaces.py",
|
|
"src/文件.py",
|
|
"src/file-name.py",
|
|
"src/file_name.py",
|
|
]
|
|
|
|
for path in paths:
|
|
# 不应该因为特殊字符而出错
|
|
result = should_exclude(path)
|
|
assert isinstance(result, bool)
|
|
|
|
def test_deep_nested_paths(self):
|
|
"""测试深层嵌套路径"""
|
|
deep_path = "a/b/c/d/e/f/g/h/i/j/main.py"
|
|
assert should_exclude(deep_path) is False
|
|
|
|
deep_excluded = "a/b/c/node_modules/d/e/f.js"
|
|
assert should_exclude(deep_excluded) is True
|
|
|
|
|
|
def run_tests():
|
|
"""运行所有测试"""
|
|
print("=" * 60)
|
|
print("文件选择与排除模式功能测试")
|
|
print("=" * 60)
|
|
|
|
# 测试 should_exclude
|
|
print("\n[1/6] 测试 should_exclude 函数...")
|
|
test_exclude = TestShouldExclude()
|
|
test_exclude.test_default_exclude_patterns()
|
|
test_exclude.test_default_not_excluded()
|
|
test_exclude.test_custom_exclude_patterns()
|
|
test_exclude.test_combined_patterns()
|
|
print("✅ should_exclude 测试通过")
|
|
|
|
# 测试 is_text_file
|
|
print("\n[2/6] 测试 is_text_file 函数...")
|
|
test_text = TestIsTextFile()
|
|
test_text.test_supported_extensions()
|
|
test_text.test_unsupported_extensions()
|
|
print("✅ is_text_file 测试通过")
|
|
|
|
# 测试排除模式集成
|
|
print("\n[3/6] 测试排除模式集成...")
|
|
test_integration = TestExcludePatternsIntegration()
|
|
test_integration.test_exclude_patterns_with_path_segments()
|
|
test_integration.test_empty_exclude_patterns()
|
|
test_integration.test_none_exclude_patterns()
|
|
print("✅ 排除模式集成测试通过")
|
|
|
|
# 测试文件选择工作流
|
|
print("\n[4/6] 测试文件选择工作流...")
|
|
test_workflow = TestFileSelectionWorkflow()
|
|
test_workflow.test_zip_file_filtering()
|
|
test_workflow.test_file_selection_with_exclude()
|
|
test_workflow.test_exclude_patterns_change_clears_selection()
|
|
print("✅ 文件选择工作流测试通过")
|
|
|
|
# 测试边界情况
|
|
print("\n[5/6] 测试边界情况...")
|
|
test_edge = TestEdgeCases()
|
|
test_edge.test_empty_file_list()
|
|
test_edge.test_all_files_excluded()
|
|
test_edge.test_special_characters_in_path()
|
|
test_edge.test_deep_nested_paths()
|
|
print("✅ 边界情况测试通过")
|
|
|
|
# 测试 API 端点(同步版本)
|
|
print("\n[6/6] 测试 API 端点参数...")
|
|
test_api = TestAPIEndpoints()
|
|
# 使用 asyncio 运行异步测试
|
|
asyncio.run(test_api.test_get_project_files_with_exclude())
|
|
asyncio.run(test_api.test_scan_request_with_exclude())
|
|
print("✅ API 端点测试通过")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("🎉 所有测试通过!")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_tests()
|