Merge branch 'main' into testbed

This commit is contained in:
2025-11-27 21:49:43 +09:00
11 changed files with 123 additions and 647 deletions
-88
View File
@@ -1,88 +0,0 @@
# 测试指南
## 运行测试
### 运行所有测试
```bash
pytest
```
### 运行特定测试文件
```bash
pytest tests/test_playlist_merge.py
```
### 运行特定测试类或函数
```bash
pytest tests/test_playlist_merge.py::TestPlaylistMergeLocalPriority
pytest tests/test_playlist_merge.py::TestPlaylistMergeLocalPriority::test_merge_local_priority
```
### 运行特定参数化测试用例
```bash
# 只运行 case1 的测试
pytest tests/test_playlist_merge.py -k "case_num-1"
```
### 查看详细输出
```bash
pytest -v # verbose
pytest -vv # more verbose
pytest -s # 显示 print 输出
```
### 查看测试覆盖率
```bash
pip install pytest-cov
pytest --cov=app --cov-report=html
```
然后在浏览器中打开 `htmlcov/index.html`
## 测试结构
- `test_playlist_merge.py` - 播放列表合并功能的单元测试
- `conftest.py` - pytest fixtures 和配置
- `pytest.ini` - pytest 配置文件
## 编写新测试
1.`tests/` 目录下创建 `test_*.py` 文件
2. 创建以 `Test` 开头的测试类
3. 编写以 `test_` 开头的测试函数
4. 使用 `assert` 进行断言
5. 使用 `@pytest.mark.parametrize` 进行参数化测试
示例:
```python
import pytest
class TestMyFeature:
def test_basic_functionality(self):
result = my_function(input_data)
assert result == expected_output
@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 4),
(3, 6),
])
def test_with_multiple_inputs(self, input, expected):
assert my_function(input) == expected
```
## 调试测试
### 在测试失败时进入调试器
```bash
pytest --pdb
```
### 只运行上次失败的测试
```bash
pytest --lf
```
### 先运行失败的测试
```bash
pytest --ff
```
-1
View File
@@ -1 +0,0 @@
# Test package initialization
-45
View File
@@ -1,45 +0,0 @@
"""
Pytest configuration and shared fixtures.
"""
import os
import shutil
from pathlib import Path
import pytest
@pytest.fixture(scope="session")
def project_root():
"""Return the project root directory."""
return Path(__file__).parent.parent
@pytest.fixture(scope="session")
def test_data_dir(project_root):
"""Return the test data directory."""
return project_root / "test_case"
@pytest.fixture(scope="session")
def expected_results_dir(project_root):
"""Return the expected results directory."""
return project_root / "test_res"
@pytest.fixture
def temp_test_dir(tmp_path):
"""Create a temporary directory for test output."""
test_dir = tmp_path / "test_output"
test_dir.mkdir(exist_ok=True)
yield test_dir
# Cleanup after test (optional)
# shutil.rmtree(test_dir, ignore_errors=True)
@pytest.fixture(autouse=True)
def reset_test_state():
"""Reset any global state before each test."""
# Add any necessary cleanup here
yield
# Post-test cleanup
-225
View File
@@ -1,225 +0,0 @@
"""
Unit tests for playlist merge functionality.
"""
import os
from pathlib import Path
import pytest
from app.utils.playlist_merge import (
merge_playlists,
ConflictResolutionStrategy,
load_paths,
save_paths,
preprocess_playlist_text,
)
# Test data directory
TEST_CASE_DIR = Path(__file__).parent.parent / "test_case"
TEST_RES_DIR = Path(__file__).parent.parent / "test_res"
DOCKERAPP_TEST_DIR = Path(__file__).parent.parent / "dockerapp" / "test_playlists"
def normalize_playlist_content(file_path: str | Path) -> list[str]:
"""
Read a playlist file and return normalized content (without comments/blanks).
Args:
file_path: Path to the playlist file
Returns:
List of track paths (non-empty, non-comment lines)
"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Remove blank lines and comments
lines = [
line.strip()
for line in content.splitlines()
if line.strip() and not line.startswith('#')
]
return lines
def assert_playlists_equal(actual_file: str | Path, expected_file: str | Path):
"""
Assert that two playlist files have identical content (ignoring comments).
Args:
actual_file: Path to the generated playlist
expected_file: Path to the expected playlist
Raises:
AssertionError: If playlists differ
"""
actual_content = normalize_playlist_content(actual_file)
expected_content = normalize_playlist_content(expected_file)
if actual_content != expected_content:
# Provide detailed diff on failure
import difflib
diff = difflib.unified_diff(
expected_content,
actual_content,
fromfile=str(expected_file),
tofile=str(actual_file),
lineterm=''
)
diff_text = '\n'.join(diff)
pytest.fail(f"Playlist files differ:\n{diff_text}")
class TestPlaylistMergeBasics:
"""Test basic playlist merge operations."""
def test_load_paths_removes_comments(self):
"""Test that load_paths correctly filters comments and blanks."""
text = "#EXTM3U\n\n/path/to/track1.mp3\n#Comment\n/path/to/track2.mp3\n"
paths = load_paths(text)
assert paths == ["/path/to/track1.mp3", "/path/to/track2.mp3"]
def test_save_paths_adds_header(self):
"""Test that save_paths adds proper m3u header."""
paths = ["/path/to/track1.mp3", "/path/to/track2.mp3"]
text = save_paths(paths)
assert text.startswith("#EXTM3U\n")
assert "/path/to/track1.mp3" in text
assert "/path/to/track2.mp3" in text
def test_empty_playlist_merge(self):
"""Test merging empty playlists."""
result = merge_playlists("", "", "", test_folder=str(DOCKERAPP_TEST_DIR / "empty_test"))
assert result.merged_paths == []
assert result.conflicts == []
def test_preprocess_playlist_text(self):
"""Ensure regex replacements run in order."""
raw = """#EXTM3U
\\\\koha9-nas\\koha9-nas\\Music\\Album\\01.flac
/music/cache/temp.flac
"""
rules = [
{"pattern": r"\\\\koha9-nas\\koha9-nas\\Music", "replacement": r"N:\\Music"},
{"pattern": r"/music/cache/", "replacement": "/data/music/"},
]
processed = preprocess_playlist_text(raw, rules)
lines = [line for line in processed.splitlines() if line and not line.startswith("#")]
assert lines[0] == r"N:\Music\Album\01.flac"
assert lines[1] == "/data/music/temp.flac"
@pytest.mark.parametrize("case_num", [1, 2, 3, 4])
class TestPlaylistMergeLocalPriority:
"""Test playlist merge with local priority strategy."""
def test_merge_local_priority(self, case_num, tmp_path):
"""Test case {case_num} with local priority."""
# Load input files
base_file = TEST_CASE_DIR / "base" / f"case{case_num}.m3u"
local_file = TEST_CASE_DIR / "local_playlist" / f"case{case_num}.m3u"
remote_file = TEST_CASE_DIR / "remote_playlist" / f"case{case_num}.m3u"
if not base_file.exists():
pytest.skip("Test playlist fixtures are not available in this environment.")
if not (local_file.exists() and remote_file.exists()):
pytest.skip("Test playlist fixtures are not available in this environment.")
with open(base_file, 'r', encoding='utf-8') as f:
base_text = f.read()
with open(local_file, 'r', encoding='utf-8') as f:
local_text = f.read()
with open(remote_file, 'r', encoding='utf-8') as f:
remote_text = f.read()
# Create test output folder
test_folder = str(tmp_path / f"case{case_num}_local")
# Perform merge
result = merge_playlists(
base_text=base_text,
local_text=local_text,
remote_text=remote_text,
strategy=ConflictResolutionStrategy.LOCAL_PRIORITY,
test_folder=test_folder
)
# Compare with expected result
expected_file = TEST_RES_DIR / f"case{case_num}_merged_local_primary.m3u"
actual_file = Path(test_folder) / "local_result.m3u8"
assert_playlists_equal(actual_file, expected_file)
@pytest.mark.parametrize("case_num", [1, 2, 3, 4])
class TestPlaylistMergeRemotePriority:
"""Test playlist merge with remote priority strategy."""
def test_merge_remote_priority(self, case_num, tmp_path):
"""Test case {case_num} with remote priority."""
# Load input files
base_file = TEST_CASE_DIR / "base" / f"case{case_num}.m3u"
local_file = TEST_CASE_DIR / "local_playlist" / f"case{case_num}.m3u"
remote_file = TEST_CASE_DIR / "remote_playlist" / f"case{case_num}.m3u"
if not base_file.exists():
pytest.skip("Test playlist fixtures are not available in this environment.")
if not (local_file.exists() and remote_file.exists()):
pytest.skip("Test playlist fixtures are not available in this environment.")
with open(base_file, 'r', encoding='utf-8') as f:
base_text = f.read()
with open(local_file, 'r', encoding='utf-8') as f:
local_text = f.read()
with open(remote_file, 'r', encoding='utf-8') as f:
remote_text = f.read()
# Create test output folder
test_folder = str(tmp_path / f"case{case_num}_remote")
# Perform merge
result = merge_playlists(
base_text=base_text,
local_text=local_text,
remote_text=remote_text,
strategy=ConflictResolutionStrategy.REMOTE_PRIORITY,
test_folder=test_folder
)
# Compare with expected result
expected_file = TEST_RES_DIR / f"case{case_num}_merged_remote_primary.m3u"
actual_file = Path(test_folder) / "remote_result.m3u8"
assert_playlists_equal(actual_file, expected_file)
class TestConflictDetection:
"""Test conflict detection in merges."""
def test_conflict_reporting(self, tmp_path):
"""Test that conflicts are properly reported."""
base = "#EXTM3U\n/track1.mp3\n"
local = "#EXTM3U\n/track2.mp3\n" # Changed
remote = "#EXTM3U\n/track3.mp3\n" # Changed differently
result = merge_playlists(
base_text=base,
local_text=local,
remote_text=remote,
strategy=ConflictResolutionStrategy.LOCAL_PRIORITY,
test_folder=str(tmp_path / "conflict_test")
)
# Should detect conflict
assert len(result.conflicts) > 0
# Local priority should prefer local version
assert "/track2.mp3" in result.merged_paths
if __name__ == "__main__":
# Allow running tests directly
pytest.main([__file__, "-v"])