test: prove the database lands at the device root, not in the music folder
Build and publish container / build (pull_request) Successful in 4m30s
Build and publish container / build (pull_request) Successful in 4m30s
The two tools disagree about where the root is. rsync copies artist folders into <device>/Music, while the database builder must run one level up, where .rockbox lives, and must record /Music/... paths despite reading the bytes from the mirror. device_prefix is what reconciles them, and until now that was only argued rather than demonstrated. Unprivileged user namespaces make a real bind mount possible, so the test can create an actual mount point and exercise the derivation instead of asserting the shape of the script. A stub builder records its working directory and what it could see. The test asserts the .tcd file arrives beside .rockbox rather than inside Music, that the build ran in the scratch root and not on the card, and that it could walk into the mirror through the symlink -- which is the mechanism that produces device paths from mirror bytes. It skips where user namespaces are unavailable, which includes the CI container.
This commit is contained in:
@@ -271,3 +271,67 @@ def test_the_scan_reads_from_the_mirror_not_the_device():
|
||||
|
||||
assert 'ln -s "$mirror"' in script
|
||||
assert 'cd "$scratch"' in script
|
||||
|
||||
|
||||
def can_bind_mount():
|
||||
"""User namespaces let an unprivileged process bind mount. Not everywhere,
|
||||
notably not inside some containers, so the test that needs it skips."""
|
||||
return (
|
||||
subprocess.run(
|
||||
["unshare", "-Umr", "true"], capture_output=True, check=False
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not can_bind_mount(), reason="needs unprivileged user namespaces")
|
||||
def test_the_database_lands_at_the_device_root_not_the_music_folder(tmp_path):
|
||||
"""The two tools disagree about where the root is. rsync copies artist
|
||||
folders into <device>/Music; the database tool must run one level up, where
|
||||
.rockbox lives, and must record /Music/... paths while reading the bytes
|
||||
from the mirror. device_prefix is what reconciles them.
|
||||
"""
|
||||
mirror = tmp_path / "mirror" / "Pendulum" / "Immersion"
|
||||
mirror.mkdir(parents=True)
|
||||
(mirror / "01.mp3").write_bytes(b"not really an mp3")
|
||||
card = tmp_path / "card"
|
||||
(card / ".rockbox").mkdir(parents=True)
|
||||
(card / "Music").mkdir()
|
||||
device = tmp_path / "device"
|
||||
device.mkdir()
|
||||
|
||||
tool = tmp_path / "fake-database"
|
||||
# Records where it was run and what it could see, which is the whole
|
||||
# question; producing a real database needs Rockbox's builder.
|
||||
tool.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"printf '%s\\n' \"$PWD\" > .rockbox/where.txt\n"
|
||||
"ls Music/ > .rockbox/saw.txt\n"
|
||||
"echo db > .rockbox/database_0.tcd\n"
|
||||
)
|
||||
tool.chmod(0o755)
|
||||
|
||||
script = (
|
||||
f"mount --bind {card} {device} && "
|
||||
f"XDG_CACHE_HOME={tmp_path / 'cache'} MUSIC_MIRROR_DATABASE_TOOL={tool} "
|
||||
f"bash {SCRIPT} -f -S -U -Q {tmp_path / 'mirror'} {device / 'Music'}"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["unshare", "-Umr", "sh", "-c", script], capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
# The database lands beside the device root, not inside Music.
|
||||
assert (card / ".rockbox" / "database_0.tcd").is_file()
|
||||
# Only *.tcd is copied across, so the markers stay in the scratch root --
|
||||
# which is itself the point: nothing else is written to the device.
|
||||
scratch = tmp_path / "cache" / "music-mirror" / "database" / ".rockbox"
|
||||
assert not (card / ".rockbox" / "where.txt").exists()
|
||||
|
||||
# It ran in the scratch root, not on the card.
|
||||
where = (scratch / "where.txt").read_text().strip()
|
||||
assert where.endswith("music-mirror/database"), where
|
||||
# ...and could walk into the mirror through a symlink named for the device
|
||||
# prefix, which is how the paths come out as /Music/... while the bytes are
|
||||
# read from somewhere else entirely.
|
||||
assert "Pendulum" in (scratch / "saw.txt").read_text()
|
||||
|
||||
Reference in New Issue
Block a user