/
/
/
1"""Tests for the filesystem provider helpers."""
2
3from pathlib import Path
4
5from music_assistant.providers.filesystem_local.helpers import (
6 FileSystemItem,
7 get_folder_signature,
8 sorted_scandir,
9)
10
11
12def _file_item(name: str, checksum: str = "1700000000", file_size: int = 1024) -> FileSystemItem:
13 """
14 Build a FileSystemItem for a file, without touching the filesystem.
15
16 :param name: Relative path of the file.
17 :param checksum: Last modified time of the file.
18 :param file_size: Size of the file in bytes.
19 """
20 return FileSystemItem(
21 filename=name.rsplit("/", 1)[-1],
22 relative_path=name,
23 absolute_path=f"/media/{name}",
24 is_dir=False,
25 checksum=checksum,
26 file_size=file_size,
27 )
28
29
30def test_sorted_scandir_natural_order(tmp_path: Path) -> None:
31 """Entries are returned in natural, case insensitive order when sort is enabled."""
32 (tmp_path / "Incoming").mkdir()
33 (tmp_path / "albums").mkdir()
34 for name in ("10 - Third.flac", "2 - Second.flac", "1 - First.flac", "cover.jpg"):
35 (tmp_path / name).touch()
36
37 result = sorted_scandir(str(tmp_path), str(tmp_path), sort=True)
38
39 assert [item.filename for item in result] == [
40 "1 - First.flac",
41 "2 - Second.flac",
42 "10 - Third.flac",
43 "albums",
44 "cover.jpg",
45 "Incoming",
46 ]
47
48
49def test_sorted_scandir_handles_non_decimal_digits(tmp_path: Path) -> None:
50 """Names with digit-like characters that int() rejects still sort without raising."""
51 names = (
52 "Cherry Moon - The Compilation 2002\u00b3",
53 "Cherry Moon - The Compilation 2001",
54 "\u2160\u2161 Roman",
55 "\u0663 Arabic-Indic",
56 )
57 for name in names:
58 (tmp_path / name).mkdir()
59
60 result = sorted_scandir(str(tmp_path), str(tmp_path), sort=True)
61
62 assert sorted(item.filename for item in result) == sorted(names)
63 # the decimal run still sorts numerically while the superscript compares as text
64 assert result.index(next(i for i in result if i.filename.endswith("2001"))) < result.index(
65 next(i for i in result if i.filename.endswith("2002\u00b3"))
66 )
67
68
69def test_sorted_scandir_unsorted_by_default(tmp_path: Path) -> None:
70 """Without the sort flag, entries are returned in raw scandir order."""
71 for name in ("b.flac", "a.flac"):
72 (tmp_path / name).touch()
73
74 result = sorted_scandir(str(tmp_path), str(tmp_path))
75
76 assert sorted(item.filename for item in result) == ["a.flac", "b.flac"]
77
78
79def test_folder_signature_ignores_item_order() -> None:
80 """The same set of files always produces the same signature."""
81 items = [_file_item("pod/ep1.mp3"), _file_item("pod/ep2.mp3")]
82
83 assert get_folder_signature(items) == get_folder_signature(list(reversed(items)))
84
85
86def test_folder_signature_detects_changes() -> None:
87 """Adding, removing, replacing or retagging a file changes the signature."""
88 items = [_file_item("pod/ep1.mp3"), _file_item("pod/ep2.mp3")]
89 signature = get_folder_signature(items)
90
91 assert get_folder_signature([*items, _file_item("pod/ep3.mp3")]) != signature
92 assert get_folder_signature(items[:1]) != signature
93 # same number of files, but one of them replaced, retagged or renamed
94 for changed in (
95 _file_item("pod/ep2.mp3", checksum="1800000000"),
96 _file_item("pod/ep2.mp3", file_size=2048),
97 _file_item("pod/renamed.mp3"),
98 ):
99 assert get_folder_signature([items[0], changed]) != signature
100
101
102def test_folder_signature_cannot_be_forged_by_a_filename() -> None:
103 """A filename spelling out another entry does not collide with the entries it names."""
104 real = [_file_item("a.mp3"), _file_item("b.mp3", checksum="1800000000", file_size=2048)]
105 forged = [_file_item("a.mp3:1700000000:1024|b.mp3", checksum="1800000000", file_size=2048)]
106
107 assert get_folder_signature(forged) != get_folder_signature(real)
108