Files
hate_crack/tests/test_progress_spinner.py
T
Justin BollingerandClaude fef606dbf4 fix(llm): fix stride sampling exactness, zero-cap crash, and test quality
- Defect 1 (main.py): Replace floating-point stride loop with exact
  floor(k * total / cap) index formula so sampling always returns
  EXACTLY cap items for any 1 <= cap <= total_usable, including the
  stride < 2 regime where the old approach returned cap-1 items.

- Defect 2 (main.py): Validate ollamaMaxSampleLines before computing
  the stride; values <= 0 are nonsensical and fall back to 500
  (same as the default) rather than raising ZeroDivisionError.

- Defect 3 (test_progress_spinner.py): Rewrite test_non_tty_no_extra_thread
  to patch threading.Thread and assert it was never called, so the test
  actually fails when the spinner starts a thread in the non-TTY path.

- Defect 4 (test_progress_spinner.py): Simplify test_non_tty_prints_message
  to use monkeypatch.setattr instead of the dead mock.patch.object +
  manual assign/restore layering; removes two # type: ignore comments.

- docs(README.md): Document ollamaMaxSampleLines alongside ollamaNumCtx /
  ollamaTimeout in the Ollama Configuration section.

- tests: Add boundary-case tests for stride < 2 (total==cap, total==cap+1,
  total=3/cap=2, cap=1) and zero-cap fallback; make
  test_ollama_max_sample_lines_default exercise behaviour rather than
  the ambient config value.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 17:57:09 -04:00

103 lines
3.3 KiB
Python

"""Tests for hate_crack/progress.py — the generic spinner context manager."""
from __future__ import annotations
import io
import sys
import time
from unittest import mock
import pytest
from hate_crack.progress import spinner
# ---------------------------------------------------------------------------
# TTY guard: non-TTY path must print message once and not start a thread
# ---------------------------------------------------------------------------
def test_non_tty_prints_message(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""In a non-TTY environment the message is printed once and no thread runs."""
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
with spinner("Working..."):
pass
captured = capsys.readouterr()
assert "Working..." in captured.out
def test_non_tty_no_extra_thread() -> None:
"""Spinner in non-TTY context must never construct a Thread."""
buf = io.StringIO()
buf.isatty = lambda: False # type: ignore[attr-defined]
with mock.patch("sys.stdout", buf), mock.patch(
"hate_crack.progress.threading.Thread"
) as mock_thread:
with spinner("No threads please"):
pass
mock_thread.assert_not_called()
# ---------------------------------------------------------------------------
# TTY path: spinner starts a thread, clears the line on exit
# ---------------------------------------------------------------------------
def test_tty_clears_line_on_exit() -> None:
"""After the context exits the spinner line must be erased."""
buf = io.StringIO()
buf.isatty = lambda: True # type: ignore[attr-defined]
with mock.patch("sys.stdout", buf):
with spinner("Testing..."):
time.sleep(0.05) # let spinner tick at least once
output = buf.getvalue()
# Line clear sequence must be present somewhere after the spinner started.
assert "\033[2K\r" in output or ("\033[2K" in output)
def test_tty_shows_elapsed_seconds() -> None:
"""The spinner should show at least a 0s counter in its output."""
buf = io.StringIO()
buf.isatty = lambda: True # type: ignore[attr-defined]
with mock.patch("sys.stdout", buf):
with spinner("Counting..."):
time.sleep(0.25) # enough time for several ticks
output = buf.getvalue()
# Should have written at least one elapsed counter like "0s" or "1s"
assert "s" in output
assert "Counting..." in output
def test_tty_exception_still_clears_line() -> None:
"""Spinner must clear the line even when the body raises."""
buf = io.StringIO()
buf.isatty = lambda: True # type: ignore[attr-defined]
with mock.patch("sys.stdout", buf):
with pytest.raises(ValueError):
with spinner("Will raise"):
raise ValueError("boom")
output = buf.getvalue()
assert "\033[2K\r" in output or ("\033[2K" in output)
def test_spinner_does_not_swallow_exception() -> None:
"""The spinner context manager must re-raise exceptions from the body."""
buf = io.StringIO()
buf.isatty = lambda: True # type: ignore[attr-defined]
with mock.patch("sys.stdout", buf):
with pytest.raises(RuntimeError, match="test error"):
with spinner("Should propagate"):
raise RuntimeError("test error")