同步逻辑,正则替换测试。

This commit is contained in:
2025-11-27 05:03:35 +09:00
parent c2b429272f
commit 58709570a9
37 changed files with 2370 additions and 3 deletions
+554
View File
@@ -0,0 +1,554 @@
"""
Unit tests for regex path replacement functionality.
测试正则替换路径功能的各种场景。
"""
import re
from pathlib import Path
import pytest
from app.utils.playlist_merge import (
_compile_regex_rules,
_apply_compiled_rules_to_paths,
apply_regex_rules_to_paths,
preprocess_playlist_text,
)
class TestCompileRegexRules:
"""测试正则规则编译功能"""
def test_compile_simple_pattern(self):
"""测试编译简单正则模式"""
rules = [{"pattern": r"foo", "replacement": "bar"}]
compiled = _compile_regex_rules(rules)
assert len(compiled) == 1
assert isinstance(compiled[0][0], re.Pattern)
assert compiled[0][1] == "bar"
def test_compile_multiple_patterns(self):
"""测试编译多个正则模式"""
rules = [
{"pattern": r"foo", "replacement": "bar"},
{"pattern": r"\d+", "replacement": "NUM"},
{"pattern": r"[A-Z]+", "replacement": "UPPER"},
]
compiled = _compile_regex_rules(rules)
assert len(compiled) == 3
def test_compile_empty_pattern_skipped(self):
"""测试跳过空模式"""
rules = [
{"pattern": "", "replacement": "bar"},
{"pattern": r"foo", "replacement": "bar"},
]
compiled = _compile_regex_rules(rules)
assert len(compiled) == 1
def test_compile_missing_pattern_skipped(self):
"""测试跳过缺失模式"""
rules = [
{"replacement": "bar"}, # no pattern
{"pattern": r"foo", "replacement": "bar"},
]
compiled = _compile_regex_rules(rules)
assert len(compiled) == 1
def test_compile_invalid_regex_skipped(self):
"""测试跳过无效正则表达式"""
rules = [
{"pattern": r"[invalid(", "replacement": "bar"}, # invalid regex
{"pattern": r"foo", "replacement": "bar"},
]
compiled = _compile_regex_rules(rules)
# Invalid pattern should be skipped
assert len(compiled) == 1
def test_compile_empty_replacement(self):
"""测试空替换字符串"""
rules = [{"pattern": r"foo", "replacement": ""}]
compiled = _compile_regex_rules(rules)
assert len(compiled) == 1
assert compiled[0][1] == ""
def test_compile_missing_replacement(self):
"""测试缺失替换字符串(默认为空)"""
rules = [{"pattern": r"foo"}]
compiled = _compile_regex_rules(rules)
assert len(compiled) == 1
assert compiled[0][1] == ""
class TestApplyCompiledRulesToPaths:
"""测试应用已编译的正则规则到路径"""
def test_apply_single_rule(self):
"""测试应用单个规则"""
paths = ["/music/album/track1.mp3", "/music/album/track2.mp3"]
compiled = [(re.compile(r"/music/"), "/data/")]
result = _apply_compiled_rules_to_paths(paths, compiled)
assert result == ["/data/album/track1.mp3", "/data/album/track2.mp3"]
def test_apply_multiple_rules_in_order(self):
"""测试按顺序应用多个规则"""
paths = ["/temp/music/file.mp3"]
compiled = [
(re.compile(r"/temp/"), "/data/"),
(re.compile(r"/data/"), "/storage/"),
]
result = _apply_compiled_rules_to_paths(paths, compiled)
# Should apply both rules in sequence
assert result == ["/storage/music/file.mp3"]
def test_apply_no_rules(self):
"""测试没有规则时返回原路径"""
paths = ["/music/track.mp3"]
compiled = []
result = _apply_compiled_rules_to_paths(paths, compiled)
assert result == paths
def test_apply_no_match(self):
"""测试规则不匹配时保持原路径"""
paths = ["/music/track.mp3"]
compiled = [(re.compile(r"/video/"), "/data/")]
result = _apply_compiled_rules_to_paths(paths, compiled)
assert result == paths
def test_apply_partial_match(self):
"""测试部分路径匹配"""
paths = [
"/music/rock/song.mp3",
"/video/movie.mp4",
"/music/jazz/tune.mp3",
]
compiled = [(re.compile(r"/music/"), "/audio/")]
result = _apply_compiled_rules_to_paths(paths, compiled)
assert result == [
"/audio/rock/song.mp3",
"/video/movie.mp4",
"/audio/jazz/tune.mp3",
]
class TestApplyRegexRulesToPaths:
"""测试完整的路径正则替换流程(含编译)"""
def test_simple_replacement(self):
"""测试简单字符串替换"""
paths = ["/old/path/file.mp3"]
rules = [{"pattern": r"/old/", "replacement": "/new/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/new/path/file.mp3"]
def test_windows_path_replacement(self):
"""测试 Windows 路径替换"""
paths = [r"C:\Music\Album\track.mp3"]
rules = [{"pattern": r"C:\\Music", "replacement": r"D:\\Audio"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == [r"D:\Audio\Album\track.mp3"]
def test_unc_path_replacement(self):
"""测试 UNC 网络路径替换"""
paths = [r"\\server\share\music\track.mp3"]
rules = [
{"pattern": r"\\\\server\\share", "replacement": r"Z:"}
]
result = apply_regex_rules_to_paths(paths, rules)
assert result == [r"Z:\music\track.mp3"]
def test_case_sensitive_replacement(self):
"""测试大小写敏感替换"""
paths = ["/Music/Track.mp3", "/music/track.mp3"]
rules = [{"pattern": r"/Music/", "replacement": "/Audio/"}]
result = apply_regex_rules_to_paths(paths, rules)
# Only exact case match should be replaced
assert result == ["/Audio/Track.mp3", "/music/track.mp3"]
def test_case_insensitive_replacement(self):
"""测试大小写不敏感替换"""
paths = ["/Music/Track.mp3", "/music/track.mp3"]
rules = [{"pattern": r"(?i)/music/", "replacement": "/Audio/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/Audio/Track.mp3", "/Audio/track.mp3"]
def test_regex_special_characters(self):
"""测试正则特殊字符"""
paths = ["/music (2024)/album/track.mp3"]
rules = [{"pattern": r"/music \(\d+\)/", "replacement": "/music/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/music/album/track.mp3"]
def test_capture_group_replacement(self):
"""测试捕获组替换"""
paths = ["/music/2024/album/track.mp3"]
rules = [{"pattern": r"/music/(\d+)/", "replacement": r"/archive/\1/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/archive/2024/album/track.mp3"]
def test_multiple_capture_groups(self):
"""测试多个捕获组"""
paths = ["/music/Rock/2024/album.mp3"]
rules = [
{"pattern": r"/music/([^/]+)/(\d+)/", "replacement": r"/\2/\1/"}
]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/2024/Rock/album.mp3"]
def test_delete_pattern(self):
"""测试删除匹配内容(替换为空)"""
paths = ["/music/temp/album/track.mp3"]
rules = [{"pattern": r"/temp", "replacement": ""}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/music/album/track.mp3"]
def test_multiple_matches_in_path(self):
"""测试路径中多次匹配"""
paths = ["/old/path/old/file.mp3"]
rules = [{"pattern": r"old", "replacement": "new"}]
result = apply_regex_rules_to_paths(paths, rules)
# Should replace all occurrences
assert result == ["/new/path/new/file.mp3"]
def test_chained_replacements(self):
"""测试链式替换"""
paths = [r"\\nas\Music\Album\track.mp3"]
rules = [
{"pattern": r"\\\\nas\\Music", "replacement": "/mnt/music"},
{"pattern": r"\\", "replacement": "/"},
]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/mnt/music/Album/track.mp3"]
def test_url_encoding_path(self):
"""测试 URL 编码路径处理"""
paths = ["/music/artist%20name/track.mp3"]
rules = [{"pattern": r"%20", "replacement": " "}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/music/artist name/track.mp3"]
def test_unicode_path(self):
"""测试 Unicode 路径"""
paths = ["/音乐/专辑/歌曲.mp3"]
rules = [{"pattern": r"/音乐/", "replacement": "/music/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == ["/music/专辑/歌曲.mp3"]
def test_empty_rules_list(self):
"""测试空规则列表"""
paths = ["/music/track.mp3"]
rules = []
result = apply_regex_rules_to_paths(paths, rules)
assert result == paths
def test_empty_paths_list(self):
"""测试空路径列表"""
paths = []
rules = [{"pattern": r"foo", "replacement": "bar"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result == []
class TestPreprocessPlaylistText:
"""测试预处理播放列表文本(含正则替换)"""
def test_preprocess_with_replacements(self):
"""测试带替换的预处理"""
text = """#EXTM3U
/old/path/track1.mp3
/old/path/track2.mp3
"""
rules = [{"pattern": r"/old/", "replacement": "/new/"}]
result = preprocess_playlist_text(text, rules)
assert "#EXTM3U" in result
assert "/new/path/track1.mp3" in result
assert "/new/path/track2.mp3" in result
assert "/old/" not in result
def test_preprocess_removes_comments(self):
"""测试预处理移除注释"""
text = """#EXTM3U
# This is a comment
/music/track1.mp3
#EXTINF:123,Artist - Track
/music/track2.mp3
"""
rules = []
result = preprocess_playlist_text(text, rules)
lines = [l for l in result.splitlines() if l and not l.startswith("#")]
assert len(lines) == 2
assert "/music/track1.mp3" in lines
assert "/music/track2.mp3" in lines
def test_preprocess_empty_text(self):
"""测试预处理空文本"""
text = ""
rules = [{"pattern": r"foo", "replacement": "bar"}]
result = preprocess_playlist_text(text, rules)
assert "#EXTM3U" in result
def test_preprocess_with_blank_lines(self):
"""测试预处理包含空行的文本"""
text = """#EXTM3U
/music/track1.mp3
/music/track2.mp3
"""
rules = []
result = preprocess_playlist_text(text, rules)
lines = [l for l in result.splitlines() if l and not l.startswith("#")]
assert len(lines) == 2
def test_preprocess_real_world_scenario(self):
"""测试真实场景:NAS 路径转换"""
text = """#EXTM3U
\\\\koha9-nas\\koha9-nas\\Music\\Rock\\track1.flac
\\\\koha9-nas\\koha9-nas\\Music\\Jazz\\track2.mp3
/music/cache/temp.flac
"""
rules = [
{"pattern": r"\\\\koha9-nas\\koha9-nas\\Music", "replacement": r"N:\\Music"},
{"pattern": r"/music/cache/", "replacement": "/data/music/"},
{"pattern": r"\\", "replacement": "/"},
]
result = preprocess_playlist_text(text, rules)
lines = [l for l in result.splitlines() if l and not l.startswith("#")]
# After all replacements, backslashes should be converted to forward slashes
assert "N:/Music/Rock/track1.flac" in lines
assert "N:/Music/Jazz/track2.mp3" in lines
assert "/data/music/temp.flac" in lines
def test_preprocess_with_compiled_rules(self):
"""测试使用预编译规则"""
text = """#EXTM3U
/old/path/track.mp3
"""
rules = [{"pattern": r"/old/", "replacement": "/new/"}]
compiled = _compile_regex_rules(rules)
result = preprocess_playlist_text(text, rules, compiled_rules=compiled)
assert "/new/path/track.mp3" in result
def test_preprocess_preserves_order(self):
"""测试预处理保持顺序"""
text = """#EXTM3U
/path/track1.mp3
/path/track2.mp3
/path/track3.mp3
"""
rules = [{"pattern": r"/path/", "replacement": "/new/"}]
result = preprocess_playlist_text(text, rules)
lines = [l for l in result.splitlines() if l and not l.startswith("#")]
assert lines[0] == "/new/track1.mp3"
assert lines[1] == "/new/track2.mp3"
assert lines[2] == "/new/track3.mp3"
class TestEdgeCases:
"""测试边界情况和异常场景"""
def test_very_long_path(self):
"""测试超长路径"""
long_path = "/music/" + "a" * 1000 + "/track.mp3"
paths = [long_path]
rules = [{"pattern": r"/music/", "replacement": "/audio/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0].startswith("/audio/")
assert len(result[0]) > 1000
def test_special_characters_in_path(self):
"""测试路径中的特殊字符"""
paths = [
"/music/artist [2024]/track (remix).mp3",
"/music/artist & band/song #1.mp3",
]
rules = [{"pattern": r"/music/", "replacement": "/audio/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/audio/artist [2024]/track (remix).mp3"
assert result[1] == "/audio/artist & band/song #1.mp3"
def test_dot_in_path(self):
"""测试路径中的点号"""
paths = ["/music/../audio/track.mp3", "/music/./track.mp3"]
rules = [{"pattern": r"\.\./", "replacement": ""}]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/music/audio/track.mp3"
def test_trailing_slash(self):
"""测试尾部斜杠"""
paths = ["/music/album/", "/music/track.mp3"]
rules = [{"pattern": r"/$", "replacement": ""}]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/music/album"
assert result[1] == "/music/track.mp3"
def test_duplicate_slashes(self):
"""测试重复斜杠"""
paths = ["/music//album///track.mp3"]
rules = [{"pattern": r"/+", "replacement": "/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/music/album/track.mp3"
def test_mixed_path_separators(self):
"""测试混合路径分隔符"""
paths = [r"C:\Music/Album\track.mp3"]
rules = [
{"pattern": r"\\", "replacement": "/"},
]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "C:/Music/Album/track.mp3"
def test_regex_metacharacters_in_replacement(self):
"""测试替换字符串中的正则元字符"""
paths = ["/music/track.mp3"]
rules = [{"pattern": r"/music/", "replacement": r"/audio$/"}]
result = apply_regex_rules_to_paths(paths, rules)
# $ in replacement should be literal
assert result[0] == r"/audio$/track.mp3"
def test_empty_string_replacement(self):
"""测试替换为空字符串"""
paths = ["/music/temp/album/track.mp3"]
rules = [{"pattern": r"temp/", "replacement": ""}]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/music/album/track.mp3"
def test_replacement_creates_invalid_path(self):
"""测试替换可能产生无效路径(但仍应执行)"""
paths = ["/music/track.mp3"]
rules = [{"pattern": r"/", "replacement": ""}]
result = apply_regex_rules_to_paths(paths, rules)
# Should still perform replacement even if result is odd
assert result[0] == "musictrack.mp3"
class TestPerformance:
"""测试性能相关场景"""
def test_large_playlist(self):
"""测试大型播放列表"""
paths = [f"/music/track{i}.mp3" for i in range(10000)]
rules = [{"pattern": r"/music/", "replacement": "/audio/"}]
result = apply_regex_rules_to_paths(paths, rules)
assert len(result) == 10000
assert all(p.startswith("/audio/") for p in result)
def test_many_rules(self):
"""测试大量规则"""
paths = ["/music/rock/2024/album/track.mp3"]
rules = [
{"pattern": r"music", "replacement": "audio"},
{"pattern": r"rock", "replacement": "genre1"},
{"pattern": r"2024", "replacement": "year"},
{"pattern": r"album", "replacement": "collection"},
{"pattern": r"track", "replacement": "song"},
]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/audio/genre1/year/collection/song.mp3"
def test_complex_regex_pattern(self):
"""测试复杂正则表达式"""
paths = [
"/music/Artist - Album (2024) [FLAC]/01. Track.flac",
"/music/Another Artist - Another Album (2023) [MP3]/02. Song.mp3",
]
rules = [
{
"pattern": r"/music/(.+?) - (.+?) \((\d+)\) \[([^\]]+)\]/",
"replacement": r"/library/\4/\3/\1/\2/"
}
]
result = apply_regex_rules_to_paths(paths, rules)
assert result[0] == "/library/FLAC/2024/Artist/Album/01. Track.flac"
assert result[1] == "/library/MP3/2023/Another Artist/Another Album/02. Song.mp3"
if __name__ == "__main__":
pytest.main([__file__, "-v"])