mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 13:23:01 +02:00
Split 18+ Python files that exceeded 500 lines into smaller modules with helper files (prefixed with _). All functions are re-exported from the original modules to maintain backward compatibility with test patches and external imports. Files split: - moviepy_showcase.py (1212 -> 302 + 3 helpers) - anki_generator.py (1174 -> 473 + 4 helpers) - test_analyze_chess_game.py (1152 -> 361 + 2 parts) - poker_modifier_app.py (1024 -> 263 + 2 helpers) - transcribe_fw.py (1007 -> 342 + 3 helpers) - music_generator.py (1002 -> 319 + 2 helpers) - translator.py (951 -> 442 + 2 helpers) - cinema_planner.py (893 -> 369 + 2 helpers) - lichess_bot/main.py (757 -> 495 + _game_logic.py) - test_translator.py (725 -> 289 + part2 + conftest) - test_lichess_api.py (680 -> 475 + part2) - learning_pipe.py (668 -> 375 + 2 helpers) - cache.py (655 -> 360 + _cache_decks.py) - analyze_chess_game.py (632 -> 463 + _move_analysis.py) - visualize_q02.py (609 -> 371 + helper) - repo_explorer.py (602 -> 347 + 2 helpers) - keyboard_coop/main.py (515 -> 416 + _dictionary.py) - scanning.py (501 -> 314 + _enforce_loop.py) All tests pass: 144 lichess_bot (100% branch coverage), 243 others. No new lint errors introduced.
362 lines
13 KiB
Python
362 lines
13 KiB
Python
"""Tests for analyze_chess_game utility and scoring functions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
import chess
|
|
import chess.engine
|
|
import pytest
|
|
|
|
from python_pkg.stockfish_analysis.analyze_chess_game import (
|
|
_auto_hash_mb,
|
|
_detect_total_mem_mb,
|
|
_parse_hash_mb,
|
|
_parse_threads,
|
|
classify_cp_loss,
|
|
extract_pgn_text,
|
|
fmt_eval,
|
|
score_to_cp,
|
|
)
|
|
|
|
|
|
class TestExtractPgnText:
|
|
"""Tests for extract_pgn_text function."""
|
|
|
|
def test_extract_pgn_after_marker(self) -> None:
|
|
"""Test extraction after PGN: marker."""
|
|
raw = "Some log stuff\nPGN:\n1. e4 e5 2. Nf3 Nc6"
|
|
result = extract_pgn_text(raw)
|
|
assert result == "1. e4 e5 2. Nf3 Nc6"
|
|
|
|
def test_extract_pgn_from_tag_line(self) -> None:
|
|
"""Test extraction from first PGN tag."""
|
|
raw = 'Log header\n[Event "Test"]\n1. e4 e5'
|
|
result = extract_pgn_text(raw)
|
|
assert result is not None
|
|
assert '[Event "Test"]' in result
|
|
assert "1. e4 e5" in result
|
|
|
|
def test_extract_pgn_from_move_number(self) -> None:
|
|
"""Test extraction from first move number."""
|
|
raw = "Some text\n1. e4 e5 2. Nf3"
|
|
result = extract_pgn_text(raw)
|
|
assert result == "1. e4 e5 2. Nf3"
|
|
|
|
def test_extract_pgn_no_match(self) -> None:
|
|
"""Test extraction returns None when no PGN found."""
|
|
raw = "No PGN content here\nJust some text"
|
|
result = extract_pgn_text(raw)
|
|
assert result is None
|
|
|
|
def test_extract_pgn_empty_after_marker(self) -> None:
|
|
"""Test extraction with empty content after marker."""
|
|
# Need double newline so splitlines creates an empty second element
|
|
raw = "PGN:\n\n"
|
|
result = extract_pgn_text(raw)
|
|
# Should fall through to tag check, then move check, then None
|
|
assert result is None
|
|
|
|
def test_extract_pgn_empty_tag_line(self) -> None:
|
|
"""Test extraction when tag line at end results in empty pgn."""
|
|
# Tag line is last line so join is just that line, which is non-empty
|
|
raw = "text\n[ ]"
|
|
result = extract_pgn_text(raw)
|
|
assert result == "[ ]"
|
|
|
|
def test_extract_pgn_pgn_marker_followed_by_tag(self) -> None:
|
|
"""Test extraction when PGN: marker is followed by tag (empty after)."""
|
|
# PGN: marker with only whitespace after, then has tag
|
|
raw = "PGN:\n \n[Event]"
|
|
result = extract_pgn_text(raw)
|
|
# Whitespace lines collapse to just "[Event]"
|
|
assert "[Event]" in (result or "")
|
|
|
|
def test_extract_pgn_only_whitespace_after_tag(self) -> None:
|
|
"""Test extraction when only whitespace after tag line."""
|
|
raw = "[Event]\n \n "
|
|
result = extract_pgn_text(raw)
|
|
# Strip makes it non-empty since [Event] is included
|
|
assert result is not None
|
|
|
|
def test_extract_pgn_only_whitespace_after_move(self) -> None:
|
|
"""Test extraction when move line followed by whitespace only."""
|
|
raw = "text\n1. \n "
|
|
result = extract_pgn_text(raw)
|
|
# "1." followed by whitespace is valid
|
|
assert result is not None
|
|
|
|
|
|
class TestScoreToCp:
|
|
"""Tests for score_to_cp function."""
|
|
|
|
def test_score_to_cp_centipawn(self) -> None:
|
|
"""Test centipawn score conversion."""
|
|
mock_score = MagicMock(spec=chess.engine.PovScore)
|
|
mock_pov = MagicMock()
|
|
mock_pov.is_mate.return_value = False
|
|
mock_pov.score.return_value = 150
|
|
mock_score.pov.return_value = mock_pov
|
|
|
|
cp, mate = score_to_cp(mock_score, pov_white=True)
|
|
assert cp == 150
|
|
assert mate is None
|
|
|
|
def test_score_to_cp_mate(self) -> None:
|
|
"""Test mate score conversion."""
|
|
mock_score = MagicMock(spec=chess.engine.PovScore)
|
|
mock_pov = MagicMock()
|
|
mock_pov.is_mate.return_value = True
|
|
mock_pov.mate.return_value = 3
|
|
mock_score.pov.return_value = mock_pov
|
|
|
|
cp, mate = score_to_cp(mock_score, pov_white=True)
|
|
assert cp is None
|
|
assert mate == 3
|
|
|
|
|
|
class TestClassifyCpLoss:
|
|
"""Tests for classify_cp_loss function."""
|
|
|
|
def test_classify_best(self) -> None:
|
|
"""Test classification of best move."""
|
|
assert classify_cp_loss(5) == "Best"
|
|
assert classify_cp_loss(10) == "Best"
|
|
|
|
def test_classify_excellent(self) -> None:
|
|
"""Test classification of excellent move."""
|
|
assert classify_cp_loss(15) == "Excellent"
|
|
assert classify_cp_loss(20) == "Excellent"
|
|
|
|
def test_classify_good(self) -> None:
|
|
"""Test classification of good move."""
|
|
assert classify_cp_loss(30) == "Good"
|
|
assert classify_cp_loss(50) == "Good"
|
|
|
|
def test_classify_inaccuracy(self) -> None:
|
|
"""Test classification of inaccuracy."""
|
|
assert classify_cp_loss(60) == "Inaccuracy"
|
|
assert classify_cp_loss(99) == "Inaccuracy"
|
|
|
|
def test_classify_mistake(self) -> None:
|
|
"""Test classification of mistake."""
|
|
assert classify_cp_loss(150) == "Mistake"
|
|
assert classify_cp_loss(299) == "Mistake"
|
|
|
|
def test_classify_blunder(self) -> None:
|
|
"""Test classification of blunder."""
|
|
assert classify_cp_loss(300) == "Blunder"
|
|
assert classify_cp_loss(500) == "Blunder"
|
|
|
|
def test_classify_unknown(self) -> None:
|
|
"""Test classification of unknown loss."""
|
|
assert classify_cp_loss(None) == "Unknown"
|
|
|
|
|
|
class TestFmtEval:
|
|
"""Tests for fmt_eval function."""
|
|
|
|
def test_fmt_eval_mate(self) -> None:
|
|
"""Test formatting mate score."""
|
|
assert fmt_eval(None, 3) == "M+3"
|
|
assert fmt_eval(None, -2) == "M-2"
|
|
|
|
def test_fmt_eval_centipawn(self) -> None:
|
|
"""Test formatting centipawn score."""
|
|
assert fmt_eval(150, None) == "+1.50"
|
|
assert fmt_eval(-200, None) == "-2.00"
|
|
assert fmt_eval(0, None) == "+0.00"
|
|
|
|
def test_fmt_eval_unknown(self) -> None:
|
|
"""Test formatting unknown score."""
|
|
assert fmt_eval(None, None) == "?"
|
|
|
|
|
|
class TestParseThreads:
|
|
"""Tests for _parse_threads function."""
|
|
|
|
def test_parse_threads_auto(self) -> None:
|
|
"""Test auto thread detection."""
|
|
assert _parse_threads("auto") is None
|
|
assert _parse_threads("max") is None
|
|
assert _parse_threads("") is None
|
|
|
|
def test_parse_threads_integer(self) -> None:
|
|
"""Test integer thread count."""
|
|
assert _parse_threads("4") == 4
|
|
assert _parse_threads("16") == 16
|
|
|
|
def test_parse_threads_minimum(self) -> None:
|
|
"""Test minimum thread count enforced."""
|
|
assert _parse_threads("0") == 1
|
|
assert _parse_threads("-1") == 1
|
|
|
|
def test_parse_threads_invalid(self) -> None:
|
|
"""Test invalid thread value."""
|
|
with pytest.raises(argparse.ArgumentTypeError):
|
|
_parse_threads("invalid")
|
|
|
|
|
|
class TestParseHashMb:
|
|
"""Tests for _parse_hash_mb function."""
|
|
|
|
def test_parse_hash_auto(self) -> None:
|
|
"""Test auto hash detection."""
|
|
assert _parse_hash_mb("auto") is None
|
|
assert _parse_hash_mb("max") is None
|
|
assert _parse_hash_mb("") is None
|
|
|
|
def test_parse_hash_integer(self) -> None:
|
|
"""Test integer hash size."""
|
|
assert _parse_hash_mb("512") == 512
|
|
assert _parse_hash_mb("2048") == 2048
|
|
|
|
def test_parse_hash_minimum(self) -> None:
|
|
"""Test minimum hash size enforced."""
|
|
assert _parse_hash_mb("8") == 16
|
|
|
|
def test_parse_hash_invalid(self) -> None:
|
|
"""Test invalid hash value."""
|
|
with pytest.raises(argparse.ArgumentTypeError):
|
|
_parse_hash_mb("invalid")
|
|
|
|
|
|
class TestDetectTotalMemMb:
|
|
"""Tests for _detect_total_mem_mb function."""
|
|
|
|
def test_detect_mem_with_psutil(self) -> None:
|
|
"""Test memory detection with psutil."""
|
|
mock_vm = MagicMock()
|
|
mock_vm.total = 16 * 1024 * 1024 * 1024 # 16 GB
|
|
|
|
with patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game.psutil"
|
|
) as mock_psutil:
|
|
mock_psutil.virtual_memory.return_value = mock_vm
|
|
result = _detect_total_mem_mb()
|
|
assert result == 16384
|
|
|
|
def test_detect_mem_psutil_exception(self) -> None:
|
|
"""Test memory detection when psutil fails - falls back to /proc."""
|
|
with (
|
|
patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game.psutil"
|
|
) as mock_psutil,
|
|
patch("python_pkg.stockfish_analysis.analyze_chess_game.Path") as mock_path,
|
|
):
|
|
mock_psutil.virtual_memory.side_effect = RuntimeError("fail")
|
|
# Also make /proc/meminfo fail so we get None
|
|
mock_path.return_value.open.side_effect = FileNotFoundError()
|
|
result = _detect_total_mem_mb()
|
|
assert result is None
|
|
|
|
def test_detect_mem_from_proc(self) -> None:
|
|
"""Test memory detection from /proc/meminfo."""
|
|
meminfo_content = "MemTotal: 16384000 kB\nMemFree: 8000000 kB"
|
|
with (
|
|
patch("python_pkg.stockfish_analysis.analyze_chess_game.psutil", None),
|
|
patch("pathlib.Path.open", mock_open(read_data=meminfo_content)),
|
|
):
|
|
result = _detect_total_mem_mb()
|
|
assert result == 16000 # 16384000 kB / 1024
|
|
|
|
def test_detect_mem_no_psutil_no_proc(self) -> None:
|
|
"""Test memory detection when both methods fail."""
|
|
with (
|
|
patch("python_pkg.stockfish_analysis.analyze_chess_game.psutil", None),
|
|
patch("pathlib.Path.open", side_effect=FileNotFoundError),
|
|
):
|
|
result = _detect_total_mem_mb()
|
|
assert result is None
|
|
|
|
def test_detect_mem_proc_no_memtotal(self) -> None:
|
|
"""Test memory detection when MemTotal line is missing."""
|
|
meminfo_content = "MemFree: 8000000 kB\nBuffers: 1000 kB"
|
|
with (
|
|
patch("python_pkg.stockfish_analysis.analyze_chess_game.psutil", None),
|
|
patch("pathlib.Path.open", mock_open(read_data=meminfo_content)),
|
|
):
|
|
result = _detect_total_mem_mb()
|
|
assert result is None
|
|
|
|
def test_detect_mem_proc_invalid_parts(self) -> None:
|
|
"""Test memory detection when MemTotal line has invalid format."""
|
|
meminfo_content = "MemTotal: notanumber kB\nMemFree: 8000000 kB"
|
|
with (
|
|
patch("python_pkg.stockfish_analysis.analyze_chess_game.psutil", None),
|
|
patch("pathlib.Path.open", mock_open(read_data=meminfo_content)),
|
|
):
|
|
result = _detect_total_mem_mb()
|
|
assert result is None
|
|
|
|
def test_detect_mem_proc_short_parts(self) -> None:
|
|
"""Test memory detection when MemTotal has too few parts."""
|
|
meminfo_content = "MemTotal:\nMemFree: 8000000 kB"
|
|
with (
|
|
patch("python_pkg.stockfish_analysis.analyze_chess_game.psutil", None),
|
|
patch("pathlib.Path.open", mock_open(read_data=meminfo_content)),
|
|
):
|
|
result = _detect_total_mem_mb()
|
|
assert result is None
|
|
|
|
|
|
class TestAutoHashMb:
|
|
"""Tests for _auto_hash_mb function."""
|
|
|
|
def test_auto_hash_basic(self) -> None:
|
|
"""Test basic auto hash calculation."""
|
|
with patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game._detect_total_mem_mb",
|
|
return_value=8192,
|
|
):
|
|
result = _auto_hash_mb(4, {})
|
|
assert result >= 64
|
|
assert result <= 4096
|
|
|
|
def test_auto_hash_high_threads(self) -> None:
|
|
"""Test auto hash with high thread count."""
|
|
with patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game._detect_total_mem_mb",
|
|
return_value=16384,
|
|
):
|
|
result = _auto_hash_mb(20, {})
|
|
assert result > 64
|
|
|
|
def test_auto_hash_respects_engine_max(self) -> None:
|
|
"""Test auto hash respects engine maximum."""
|
|
mock_opt = MagicMock()
|
|
mock_opt.max = 256
|
|
with patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game._detect_total_mem_mb",
|
|
return_value=8192,
|
|
):
|
|
result = _auto_hash_mb(4, {"Hash": mock_opt})
|
|
assert result <= 256
|
|
|
|
def test_auto_hash_no_mem_info(self) -> None:
|
|
"""Test auto hash when memory detection fails."""
|
|
with patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game._detect_total_mem_mb",
|
|
return_value=None,
|
|
):
|
|
result = _auto_hash_mb(4, {})
|
|
assert result >= 64
|
|
|
|
def test_auto_hash_attribute_error(self) -> None:
|
|
"""Test auto hash when opt.max raises AttributeError."""
|
|
|
|
class NoMaxOpt:
|
|
"""Object without max attribute."""
|
|
|
|
@property
|
|
def max(self) -> int:
|
|
raise AttributeError
|
|
|
|
with patch(
|
|
"python_pkg.stockfish_analysis.analyze_chess_game._detect_total_mem_mb",
|
|
return_value=8192,
|
|
):
|
|
result = _auto_hash_mb(4, {"Hash": NoMaxOpt()})
|
|
assert result >= 64
|