同步逻辑,正则替换测试。
This commit is contained in:
@@ -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"])
|
||||
Reference in New Issue
Block a user