From 408beefc661c65c97444a21938512ea47de2f177 Mon Sep 17 00:00:00 2001 From: Fuxino Date: Wed, 21 Oct 2026 22:40:17 +0200 Subject: [PATCH] Keep only rsync's summary lines when not verbose Every output line was kept in memory although only the last two were logged, and fewer than two lines (e.g. with --rsync-options q) raised an IndexError. --- simple_backup/simple_backup.py | 9 ++++++--- tests/test_local.py | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/simple_backup/simple_backup.py b/simple_backup/simple_backup.py index 2aede59..2859ae3 100755 --- a/simple_backup/simple_backup.py +++ b/simple_backup/simple_backup.py @@ -24,6 +24,7 @@ import re import shlex import sys import warnings +from collections import deque from collections.abc import Callable, Iterator from datetime import datetime from functools import wraps @@ -774,7 +775,8 @@ class Backup: assert p.stdin is not None and isinstance(p.stdout, io.BufferedIOBase) p.stdin.close() - output = [] + # Without -v, only the summary (the last two lines) is logged + output: list[str] | deque[str] = [] if self._verbose else deque(maxlen=2) try: for r in _read_records(p.stdout): @@ -790,8 +792,9 @@ class Backup: if self._verbose: logger.info('rsync: %s', output) else: - logger.info('rsync: %s', output[-2]) - logger.info('rsync: %s', output[-1]) + # May be fewer than two lines, e.g. with --rsync-options q + for r in output: + logger.info('rsync: %s', r) else: self._returncode_log(returncode) diff --git a/tests/test_local.py b/tests/test_local.py index 63ce460..972f408 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -298,12 +298,13 @@ class FakePopen: returncode = 0 calls: list[list[str]] = [] + output = b'sending incremental file list\nsent 1 bytes received 2 bytes\ntotal size is 3\n' def __init__(self, args: list[str], **kwargs: Any) -> None: FakePopen.calls.append(args) os.makedirs(args[args.index('/') + 1]) self.stdin = io.BytesIO() - self.stdout = io.BytesIO(b'sending incremental file list\nsent 1 bytes received 2 bytes\ntotal size is 3\n') + self.stdout = io.BytesIO(FakePopen.output) def __enter__(self) -> 'FakePopen': return self @@ -343,6 +344,18 @@ class TestReturnCodes: assert f'--link-dest={out}/simple_backup/{old_backups[-1]}' in args assert args[args.index('/') + 1] == f'{out}/simple_backup/2026-10-14_10-00-00' + @pytest.mark.parametrize('lines, logged', [ + ([f'file{n}' for n in range(5)] + ['sent 1 bytes', 'total size is 3'], ['sent 1 bytes', 'total size is 3']), + ([], []), + (['total size is 3'], ['total size is 3']), + ]) + def test_summary_logged(self, src: Path, out: Path, fake_popen: type[FakePopen], monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, lines: list[str], logged: list[str]) -> None: + monkeypatch.setattr(fake_popen, 'output', ''.join(f'{line}\n' for line in lines).encode()) + + assert run_backup(out, [str(src)]) == 0 + assert [m[7:] for m in caplog.messages if m.startswith('rsync: ')] == logged + def test_rsync_missing(self, src: Path, out: Path, tmp_path: Path, fake_popen: type[FakePopen], old_backups: list[str], monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture) -> None: