feat: show which album is copying, and how far through
Build and publish container / build (pull_request) Successful in 2m33s
Build and publish container / build (pull_request) Successful in 2m33s
The transfer looked hung. rsync prints nothing while it builds its file list, which on fifty thousand files over USB is several minutes of silence, and --info=progress2 does not help: with incremental recursion its percentage is computed against a list rsync has not finished discovering, so it moves backwards as often as forwards. The script now counts what needs copying first and says so, then renders its own single line that rewrites in place, showing the album currently going across and a percentage against a total that is actually known. Counting costs a second pass over the tree. That is the price of a percentage meaning something, and it is cheaper than staring at a blank terminal wondering whether the thing has died. Directories are excluded from the count. rsync reports those too, and including them puts the figure past a hundred per cent. Piped to a log the line becomes a plain one every thirty seconds, because a log full of carriage returns and escape codes is not a log anybody reads.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "tools"))
|
||||
|
||||
import rsync_progress # noqa: E402
|
||||
|
||||
|
||||
class NotATerminal(io.StringIO):
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
|
||||
class Terminal(io.StringIO):
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
|
||||
def run(lines, total=0, out=None):
|
||||
out = out or NotATerminal()
|
||||
rsync_progress.main(["--total", str(total)], stream=io.StringIO(lines), out=out)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def test_the_artist_and_album_are_pulled_from_the_path():
|
||||
assert rsync_progress.album_of("Pendulum/Immersion/01 - Watercolour.mp3") == (
|
||||
"Pendulum / Immersion"
|
||||
)
|
||||
|
||||
|
||||
def test_a_shallower_path_degrades_rather_than_failing():
|
||||
assert rsync_progress.album_of("Pendulum/loose.mp3") == "Pendulum"
|
||||
assert rsync_progress.album_of("loose.mp3") == ""
|
||||
|
||||
|
||||
def test_directories_are_not_counted():
|
||||
"""rsync reports them too, and counting them puts the percentage past 100."""
|
||||
output = run("Artist/\nArtist/Album/\nArtist/Album/track.mp3\n", total=1)
|
||||
|
||||
assert "1 / 1" in output
|
||||
assert "100%" in output
|
||||
|
||||
|
||||
def test_the_percentage_tracks_the_total():
|
||||
output = run("".join(f"A/B/{i}.mp3\n" for i in range(5)), total=10)
|
||||
|
||||
assert "5 / 10" in output
|
||||
assert " 50%" in output
|
||||
|
||||
|
||||
def test_without_a_total_it_counts_instead_of_guessing():
|
||||
output = run("A/B/one.mp3\nA/B/two.mp3\n")
|
||||
|
||||
assert "2 files" in output
|
||||
assert "%" not in output
|
||||
|
||||
|
||||
def test_a_final_line_is_always_printed():
|
||||
"""Otherwise the last state of a rewriting line is whatever it happened to
|
||||
be when the interval last elapsed."""
|
||||
output = run("A/B/one.mp3\n", total=1)
|
||||
|
||||
assert output.endswith("\n")
|
||||
assert "1 / 1" in output
|
||||
|
||||
|
||||
def test_nothing_transferred_still_reports():
|
||||
output = run("", total=0)
|
||||
|
||||
assert "0 files" in output
|
||||
|
||||
|
||||
def test_a_log_gets_no_carriage_returns():
|
||||
"""A non-terminal filling with \\r and escape codes is unreadable."""
|
||||
output = run("".join(f"A/B/{i}.mp3\n" for i in range(50)), total=50)
|
||||
|
||||
assert "\r" not in output
|
||||
assert "\033" not in output
|
||||
|
||||
|
||||
def test_a_terminal_rewrites_one_line():
|
||||
output = run("".join(f"A/B/{i}.mp3\n" for i in range(50)), total=50, out=Terminal())
|
||||
|
||||
assert "\r\033[2K" in output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "width", "expected"),
|
||||
[
|
||||
("short", 20, "short"),
|
||||
("King Gizzard / PetroDragonic Apocalypse", 20, "…agonic Apocalypse"),
|
||||
],
|
||||
)
|
||||
def test_long_labels_are_trimmed_from_the_left(text, width, expected):
|
||||
"""The album is the informative end, so the artist is what gets cut."""
|
||||
trimmed = rsync_progress.fit(text, width)
|
||||
|
||||
assert len(trimmed) <= width
|
||||
if len(text) > width:
|
||||
assert trimmed.startswith("…")
|
||||
assert text.endswith(trimmed.lstrip("…"))
|
||||
Reference in New Issue
Block a user