mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-29 07:00:33 -07:00
- 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>
103 lines
3.3 KiB
Python
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")
|