arborist/tests/test_qa_progress.py
russell@unturf.com 192793d187
tests/qa/progress: 31 tests for Progress emitter (was zero coverage)
arborist/qa/progress.py — stage-level state-machine emitter for
arborist query. 85 LOC, no direct tests. Used throughout the QA
runner + ingest pipelines for stderr-only progress lines (stdout
stays clean for JSON / unfirehose journal output).

Coverage:
  - Progress.emit formatting: bare-prefix (no kvs), key=value tail
    (single + multi), elapsed timestamp format, stream redirect
  - enabled=False is true no-op (no string formatting cost)
  - default stream is sys.stderr; default t_start is monotonic-now
  - disabled() returns singleton (suitable as default kwarg)
  - from_env precedence: cli_override > ARBORIST_PROGRESS env >
    TTY auto-detect
  - 7 truthy env spellings (1/true/on/yes + case variants) enable
  - 7 falsy env spellings (0/false/off/no + case variants) disable
  - unknown / empty / whitespace env values fall through to TTY
  - sys.stderr lacking .isatty falls back to disabled (fail-closed)
  - from_env returns fresh instance per call (no shared state)
2026-05-10 12:45:32 -04:00

226 lines
7.2 KiB
Python

"""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