refactor: address code-quality review findings on ui-polish branch

- Remove review-artifact "Defect 2" comment in main.py:2076; replace with
  accurate description of the invalid-cap fallback.  Same label removed from
  the test section header in test_ollama_wordlist_sampling.py.
- Lift nested _usable helper to module-level _usable_plaintext in main.py,
  placed alongside the other private wordlist helpers after _wordlist_path.
  Add six direct unit tests covering blank, whitespace, plain, hash:pw,
  multi-colon, and empty-plaintext cases.
- Move spinner line-clear before thread.join() in progress.py so the terminal
  is cleaned up even if join() is interrupted by a DoubleInterrupt/second Ctrl-C.
  Add test_tty_clears_line_even_if_join_raises to cover this path.
- Replace time.sleep-based elapsed-counter test with a deterministic version
  that patches time.monotonic; assert on actual rendered format with r"\d+s".
  Drop the misleading 0.05s sleep in test_tty_clears_line_on_exit (shorter
  than one tick, so no frame was ever guaranteed).
- Add missing ollama* keys to hate_crack/config.json.example (package-data
  copy); root config.json.example already had them.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-07-24 18:08:11 -04:00
co-authored by Claude
parent fef606dbf4
commit 142c1fc99f
5 changed files with 123 additions and 22 deletions
+36 -1
View File
@@ -250,7 +250,7 @@ def test_stride_cap_one(env) -> None:
# ---------------------------------------------------------------------------
# Defect 2: ZeroDivisionError guard — cap=0 falls back to 500
# Invalid-cap guard — zero/negative ollamaMaxSampleLines falls back to 500
# ---------------------------------------------------------------------------
@@ -338,3 +338,38 @@ def test_spinner_called_with_model_message(env, capsys):
captured = capsys.readouterr()
assert MODEL in captured.out
assert "Generating password candidates via Ollama" in captured.out
# ---------------------------------------------------------------------------
# _usable_plaintext unit tests
# ---------------------------------------------------------------------------
def test_usable_plaintext_blank_line():
"""A blank line must return an empty string (discarded)."""
assert hc_main._usable_plaintext("") == ""
def test_usable_plaintext_whitespace_only():
"""A whitespace-only line must return an empty string (discarded)."""
assert hc_main._usable_plaintext(" \t ") == ""
def test_usable_plaintext_plain_password():
"""A plain password line (no colon) is returned stripped."""
assert hc_main._usable_plaintext("hunter2\n") == "hunter2"
def test_usable_plaintext_hash_colon_password():
"""A hash:password line returns only the password portion."""
assert hc_main._usable_plaintext("aabbcc:hunter2") == "hunter2"
def test_usable_plaintext_multiple_colons():
"""A line with multiple colons splits only on the first colon."""
assert hc_main._usable_plaintext("aabbcc:p@ss:word") == "p@ss:word"
def test_usable_plaintext_hash_colon_empty():
"""A hash: line with no plaintext after the colon returns empty string."""
assert hc_main._usable_plaintext("aabbcc:") == ""
+55 -6
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import io
import re
import sys
import time
from unittest import mock
@@ -55,7 +56,7 @@ def test_tty_clears_line_on_exit() -> None:
with mock.patch("sys.stdout", buf):
with spinner("Testing..."):
time.sleep(0.05) # let spinner tick at least once
pass # no sleep needed — the finally block writes the escape regardless
output = buf.getvalue()
# Line clear sequence must be present somewhere after the spinner started.
@@ -63,18 +64,29 @@ def test_tty_clears_line_on_exit() -> None:
def test_tty_shows_elapsed_seconds() -> None:
"""The spinner should show at least a 0s counter in its output."""
"""The spinner must render the elapsed-seconds counter in the expected format.
time.monotonic is patched so the test is deterministic regardless of
scheduling: the first call returns 0.0 (start), the second returns 3.0
(first tick), giving a stable "3s" label without sleeping.
"""
buf = io.StringIO()
buf.isatty = lambda: True # type: ignore[attr-defined]
with mock.patch("sys.stdout", buf):
# Patch monotonic: start=0.0, then 3.0 at the first tick, then keep
# returning 3.0 so stop_event.wait() ends quickly.
mono_values = iter([0.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0])
with mock.patch("sys.stdout", buf), mock.patch(
"hate_crack.progress.time.monotonic", side_effect=mono_values
):
with spinner("Counting..."):
time.sleep(0.25) # enough time for several ticks
pass # body exits immediately; the thread gets one tick then stops
output = buf.getvalue()
# Should have written at least one elapsed counter like "0s" or "1s"
assert "s" in output
assert "Counting..." in output
# At least one frame must match the "Ns" elapsed format (e.g. "3s").
assert re.search(r"\d+s", output), f"Expected elapsed counter in output: {output!r}"
def test_tty_exception_still_clears_line() -> None:
@@ -100,3 +112,40 @@ def test_spinner_does_not_swallow_exception() -> None:
with pytest.raises(RuntimeError, match="test error"):
with spinner("Should propagate"):
raise RuntimeError("test error")
# ---------------------------------------------------------------------------
# Teardown robustness: line must be cleared even if join() is interrupted
# ---------------------------------------------------------------------------
def test_tty_clears_line_even_if_join_raises() -> None:
"""Line-clear must happen even when thread.join() raises (e.g. DoubleInterrupt).
The fix moves the write("\033[2K\r") *before* thread.join() so an
exception during join cannot prevent the terminal from being cleaned up.
"""
buf = io.StringIO()
buf.isatty = lambda: True # type: ignore[attr-defined]
class FakeThread:
def __init__(self, *args, **kwargs) -> None:
pass
def start(self) -> None:
pass
def join(self, *args, **kwargs) -> None:
raise KeyboardInterrupt("simulated double Ctrl-C during join")
with mock.patch("hate_crack.progress.threading.Thread", FakeThread), mock.patch(
"sys.stdout", buf
):
with pytest.raises(KeyboardInterrupt):
with spinner("Robust teardown"):
pass
output = buf.getvalue()
assert "\033[2K\r" in output, (
"Line-clear escape sequence must be written before join() is called"
)