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)
This commit is contained in:
parent
b17900cb33
commit
192793d187
1 changed files with 226 additions and 0 deletions
226
tests/test_qa_progress.py
Normal file
226
tests/test_qa_progress.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue