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)
226 lines
7.2 KiB
Python
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
|