from __future__ import annotations from io import StringIO import pytest from rich.console import Console from kneiff.cli.progress import CliProgress from kneiff.progress import ProgressUpdate, report_progress def test_progress_update_rejects_negative_work() -> None: with pytest.raises(ValueError, match="advance"): ProgressUpdate("invalid", advance=-1) with pytest.raises(ValueError, match="total"): ProgressUpdate("invalid", additional_total=-1) def test_report_progress_sends_one_immutable_event() -> None: updates: list[ProgressUpdate] = [] report_progress( updates.append, "Discovered work", advance=2, additional_total=3, ) assert updates == [ ProgressUpdate( description="Discovered work", advance=2, additional_total=3, ) ] def test_cli_progress_applies_dynamic_total_in_terminal() -> None: output = StringIO() console = Console( file=output, force_terminal=True, color_system=None, width=100, ) with CliProgress("Preparing", console=console) as progress: assert progress.is_terminal is True progress.apply( ProgressUpdate( description="Image 1/2", advance=1, additional_total=2, ) ) progress.apply(ProgressUpdate(description="Image 2/2", advance=1)) assert progress.total == 2 rendered = output.getvalue() assert "Image 2/2" in rendered assert "2/2" in rendered def test_cli_progress_is_silent_when_redirected_except_explicit_logs() -> None: output = StringIO() console = Console(file=output, force_terminal=False, color_system=None) with CliProgress("Hidden", total=1, console=console) as progress: assert progress.is_terminal is False progress.apply(ProgressUpdate(description="Done", advance=1)) progress.log("suppressed", echo=False) assert output.getvalue() == "" with CliProgress("Hidden", console=console) as progress: progress.log("visible") assert output.getvalue() == "visible\n"