Source code for tests.test_progress

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

from __future__ import annotations

import logging
import re
import sys

import click
import pytest
from extra_platforms.pytest import skip_windows

import click_extra
from click_extra import command, echo
from click_extra.capture.recording import TerminalScreen, record_command
from click_extra.context import START_TIME
from click_extra.logging import StreamHandler as ExtraStreamHandler
from click_extra.spinner import _active_line

from .conftest import TTYStringIO


[docs] @pytest.mark.parametrize( ("args", "expected_hidden"), ( # Shown by default. ((), False), # --no-progress hides the determinate bar, just like the spinner. (("--no-progress",), True), # Color is decoupled: --no-color keeps the bar, only stripping its color. (("--no-color",), False), # --accessible hides it: a screen reader wants no animated bar. (("--accessible",), True), ), ) def test_progressbar_follows_progress_flag(invoke, args, expected_hidden): """click_extra.progressbar gates ``hidden`` on the resolved --progress flag.""" @command def cli(): bar = click_extra.progressbar([1, 2, 3], label="Brewing tea") echo(f"hidden={bar.hidden}") result = invoke(cli, *args) assert f"hidden={expected_hidden}" in result.stdout
[docs] @pytest.mark.parametrize("forced", (True, False)) def test_progressbar_explicit_hidden_overrides_flag(invoke, forced): """An explicit ``hidden=`` wins over --no-progress, like echo(color=...).""" @command def cli(): bar = click_extra.progressbar([1, 2, 3], hidden=forced) echo(f"hidden={bar.hidden}") # --no-progress would otherwise force hidden=True; the explicit value stands. result = invoke(cli, "--no-progress") assert f"hidden={forced}" in result.stdout
[docs] def test_progressbar_shown_without_active_context(): """Outside a Click command the bar defaults to shown, like click.progressbar.""" bar = click_extra.progressbar([1, 2, 3]) assert bar.hidden is False
[docs] @pytest.mark.parametrize( ("args", "label_shown"), ( ((), True), (("--no-progress",), False), ), ) def test_progressbar_label_emission_off_tty(invoke, args, label_shown): """Off a TTY a shown bar still emits its label once; a hidden one emits nothing.""" @command def cli(): with click_extra.progressbar([1, 2, 3], label="Brewing tea") as bar: for _ in bar: pass result = invoke(cli, *args) assert ("Brewing tea" in result.output) is label_shown
[docs] def test_progressbar_shows_final_position_with_update_min_steps(): """With an `update_min_steps` that does not divide the length, the bar still lands on total/total instead of the last multiple (14/20 for a length of 20 and an `update_min_steps` of 7).""" stream = TTYStringIO() with click_extra.progressbar( range(20), show_pos=True, update_min_steps=7, file=stream ) as bar: for _ in bar: pass output = click.unstyle(stream.getvalue()) assert "20/20" in output
[docs] def test_progressbar_prints_a_log_record_above_itself(monkeypatch): """A record logged while a bar draws lands on a line of its own, above it. Click draws a bar with a carriage return and no line feed until it finishes, so a record written in between continued the bar's row, after its position. """ terminal = TTYStringIO() monkeypatch.setattr(sys, "stderr", terminal) handler = ExtraStreamHandler() handler.stream = sys.stderr logger = logging.getLogger("click_extra.test.progressbar") logger.addHandler(handler) logger.propagate = False try: with click_extra.progressbar(range(3), label="Sorting plums") as bar: # Compared by identity: the registry types its entries as live lines. assert id(_active_line(terminal)) == id(bar) for _ in bar: logger.warning("Opening the fruit basket") finally: logger.removeHandler(handler) assert _active_line(terminal) is None screen = TerminalScreen() screen.feed(terminal.getvalue()) rows = [click.unstyle(row) for row in screen.rows] records = [row for row in rows if "Opening the fruit basket" in row] assert len(records) == 3 assert not [row for row in records if "Sorting plums" in row], rows # The bar keeps drawing below the records, and ends there. assert [row for row in rows if row.strip()][-1].startswith("Sorting plums")
LOGGING_UNDER_A_BAR_SCRIPT = """ import logging import time from click_extra import command, progressbar @command def sort_plums(): with progressbar(range(3), label="Sorting plums") as bar: for _ in bar: logging.warning("Opening the fruit basket") time.sleep(0.05) sort_plums() """ """A CLI logging from inside an open progress bar."""
[docs] @skip_windows(reason="A pseudo-terminal needs termios, which Windows lacks") def test_progressbar_logging_on_a_pseudo_terminal(): """On a real terminal, each record a CLI logs under a bar gets its own row.""" frames = record_command( (sys.executable, "-c", LOGGING_UNDER_A_BAR_SCRIPT), columns=80, rows=12 ) rows = [click.unstyle(row).rstrip() for row in frames[-1].text.splitlines()] records = [row for row in rows if "Opening the fruit basket" in row] assert records == ["warning: Opening the fruit basket"] * 3, rows assert [row for row in rows if row][-1].startswith("Sorting plums"), rows
[docs] def test_progressbar_draws_on_stderr_by_default(): """The bar draws where log records and the other live lines go.""" assert click_extra.progressbar([1, 2, 3]).file is sys.stderr assert click_extra.progressbar([1, 2, 3], file=sys.stdout).file is sys.stdout
def _drawn_positions(stream: TTYStringIO) -> list[str]: """The `done/total` tallies a bar drew on `stream`, in order.""" plain = click.unstyle(stream.getvalue()) return re.findall(r"(\d+/\d+)", plain)
[docs] def test_progressbar_draws_fast_items_once_per_interval(): """Items faster than the interval draw nothing between the first and the last position, however many there are.""" stream = TTYStringIO() bar = click_extra.progressbar(range(1000), show_pos=True, file=stream) bar.clock = lambda: 0.0 # type: ignore[attr-defined] with bar: for index, _ in enumerate(bar, 1): # The position moves on every step, drawn or not. assert bar.pos == index - 1 assert _drawn_positions(stream) == ["0/1000", "1000/1000"]
[docs] def test_progressbar_draws_every_slow_item(): """Items slower than the interval each draw their position, so a bar over slow items never looks frozen.""" stream = TTYStringIO() now = [0.0] def clock() -> float: now[0] += 1.0 return now[0] bar = click_extra.progressbar(range(300), show_pos=True, file=stream) bar.clock = clock # type: ignore[attr-defined] with bar: for _ in bar: pass assert _drawn_positions(stream) == [f"{done}/300" for done in range(301)]
[docs] def test_progressbar_keeps_an_explicit_update_min_steps(): """A caller stating `update_min_steps` keeps Click's gate on a count of items.""" assert click_extra.progressbar(range(10)).time_gated is True # type: ignore[attr-defined] bar = click_extra.progressbar(range(10), update_min_steps=4) assert bar.time_gated is False # type: ignore[attr-defined] assert bar.update_min_steps == 4
[docs] def test_progressbar_show_eta_follows_time_flag(): """The bar's ETA follows --time by default; an explicit show_eta wins.""" # No command context: ETA off. assert click_extra.progressbar([1, 2, 3]).show_eta is False with click.Context(click.Command("noop")) as ctx: # --no-time / default: no marker → off. assert click_extra.progressbar([1, 2, 3]).show_eta is False ctx.meta[START_TIME] = 1.0 # --time sets the marker. assert click_extra.progressbar([1, 2, 3]).show_eta is True # An explicit value overrides the flag. assert click_extra.progressbar([1, 2, 3], show_eta=False).show_eta is False