""" 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"])