"""Tests for ``arborist.qa.progress`` — stage-level state-machine emitter for ``arborist query``. Module was zero-coverage. 85 LOC of pure stdlib emitter + env/TTY/cli-override resolution. Surface: - Progress.emit (formatting, key=value tail, stream redirect) - disabled() singleton (true no-op, no formatting cost) - from_env(cli_override=...) precedence resolution - ARBORIST_PROGRESS env var (multiple truthy/falsy spellings) - TTY auto-detect fallback """ from __future__ import annotations import io import time import pytest from arborist.qa.progress import Progress, disabled, from_env # --- Progress.emit -------------------------------------------------- def test_progress_emit_writes_to_stream(): buf = io.StringIO() p = Progress(enabled=True, t_start=time.monotonic(), stream=buf) p.emit("stage.name", k="v") out = buf.getvalue() assert "stage.name" in out assert "k=v" in out assert "arborist" in out # prefix def test_progress_emit_disabled_writes_nothing(): """`enabled=False` is a true no-op — no string formatting, no stream write.""" buf = io.StringIO() p = Progress(enabled=False, t_start=time.monotonic(), stream=buf) p.emit("stage", a=1) assert buf.getvalue() == "" def test_progress_emit_no_kv_writes_bare_prefix(): """emit('stage') with no kwargs → just the timestamp + stage.""" buf = io.StringIO() p = Progress(enabled=True, t_start=time.monotonic(), stream=buf) p.emit("stage.bare") out = buf.getvalue() assert "stage.bare" in out assert "=" not in out # no key=value tail def test_progress_emit_includes_elapsed_seconds(): """Elapsed is wall-clock since t_start. Format `NN.NNs`.""" t0 = time.monotonic() - 1.5 # pretend started 1.5s ago buf = io.StringIO() p = Progress(enabled=True, t_start=t0, stream=buf) p.emit("now") out = buf.getvalue() # Format includes "1.5" or higher; exact value drifts but the # decimal format is fixed at 6.2f. assert "s]" in out # closing bracket on elapsed # Token before "s]" should parse as a positive float. head = out.split("s]")[0] elapsed_str = head.split()[-1] assert float(elapsed_str) >= 1.5 def test_progress_emit_multiple_kv_pairs_space_separated(): buf = io.StringIO() p = Progress(enabled=True, t_start=time.monotonic(), stream=buf) p.emit("multi", a=1, b="two", c=3.14) out = buf.getvalue() assert "a=1" in out assert "b=two" in out assert "c=3.14" in out def test_progress_emit_default_stream_is_stderr(capsys): """When stream kwarg is None, default is sys.stderr.""" p = Progress(enabled=True, t_start=time.monotonic(), stream=None) p.emit("default-stream") captured = capsys.readouterr() assert captured.out == "" assert "default-stream" in captured.err def test_progress_emit_default_t_start_is_now(): """If t_start kwarg is None, defaults to monotonic() at construction. Elapsed at first emit() should be ~0.""" buf = io.StringIO() p = Progress(enabled=True, t_start=None, stream=buf) p.emit("right-after-init") out = buf.getvalue() elapsed_str = out.split("s]")[0].split()[-1] assert float(elapsed_str) < 0.1 # virtually no time elapsed # --- disabled() singleton ------------------------------------------- def test_disabled_returns_singleton(): """disabled() returns the same Progress instance on every call. Suitable as a default kwarg value with no allocation.""" a = disabled() b = disabled() assert a is b assert a.enabled is False def test_disabled_emits_nothing(): p = disabled() # disabled() singleton has no test-injectable stream; emit must # still no-op without raising. p.emit("stage", k="v") # No assertion — just confirming no exception path. # --- from_env precedence -------------------------------------------- def test_from_env_cli_override_on(monkeypatch): """cli_override='on' wins over env + TTY.""" monkeypatch.setenv("ARBORIST_PROGRESS", "0") p = from_env(cli_override="on") assert p.enabled is True def test_from_env_cli_override_off(monkeypatch): """cli_override='off' wins over env.""" monkeypatch.setenv("ARBORIST_PROGRESS", "1") p = from_env(cli_override="off") assert p.enabled is False @pytest.mark.parametrize("env_value", ["1", "true", "on", "yes", "TRUE", "On", "YES"]) def test_from_env_truthy_env_enables(monkeypatch, env_value): """ARBORIST_PROGRESS in {1, true, on, yes} (case-insensitive) → enabled=True.""" monkeypatch.setenv("ARBORIST_PROGRESS", env_value) p = from_env() assert p.enabled is True @pytest.mark.parametrize("env_value", ["0", "false", "off", "no", "FALSE", "Off", "NO"]) def test_from_env_falsy_env_disables(monkeypatch, env_value): monkeypatch.setenv("ARBORIST_PROGRESS", env_value) p = from_env() assert p.enabled is False def test_from_env_unknown_env_falls_through_to_tty(monkeypatch): """ARBORIST_PROGRESS=garbage → fall through to TTY auto-detect.""" monkeypatch.setenv("ARBORIST_PROGRESS", "maybe") # Force stderr to be non-TTY (we're in a test, so usually it is). class _NonTTY: def isatty(self): return False monkeypatch.setattr("arborist.qa.progress.sys.stderr", _NonTTY()) p = from_env() assert p.enabled is False def test_from_env_unset_env_falls_through_to_tty(monkeypatch): """No ARBORIST_PROGRESS in env → TTY detect.""" monkeypatch.delenv("ARBORIST_PROGRESS", raising=False) class _IsATTY: def isatty(self): return True monkeypatch.setattr("arborist.qa.progress.sys.stderr", _IsATTY()) p = from_env() assert p.enabled is True def test_from_env_isatty_attribute_error_disables(monkeypatch): """If sys.stderr lacks .isatty (replaced with a buffer), the fallback is enabled=False (fail-closed).""" monkeypatch.delenv("ARBORIST_PROGRESS", raising=False) class _NoIsATTY: pass monkeypatch.setattr("arborist.qa.progress.sys.stderr", _NoIsATTY()) p = from_env() assert p.enabled is False def test_from_env_returns_fresh_instance(monkeypatch): """Each from_env() call returns a new Progress with a fresh t_start; never shares state with prior calls.""" monkeypatch.setenv("ARBORIST_PROGRESS", "1") a = from_env() time.sleep(0.001) b = from_env() assert a is not b assert b.t_start >= a.t_start # --- empty-string env ----------------------------------------------- def test_from_env_empty_string_falls_through(monkeypatch): """ARBORIST_PROGRESS='' (empty) → fall through to TTY.""" monkeypatch.setenv("ARBORIST_PROGRESS", "") class _NonTTY: def isatty(self): return False monkeypatch.setattr("arborist.qa.progress.sys.stderr", _NonTTY()) p = from_env() assert p.enabled is False def test_from_env_whitespace_env_falls_through(monkeypatch): """ARBORIST_PROGRESS=' ' (whitespace) → strip → empty → TTY.""" monkeypatch.setenv("ARBORIST_PROGRESS", " ") class _IsATTY: def isatty(self): return True monkeypatch.setattr("arborist.qa.progress.sys.stderr", _IsATTY()) p = from_env() assert p.enabled is True