mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 17:23:09 +02:00
- Remove all # type: ignore and # noqa comments (banned by no-noqa hook) - Add mypy --disable-error-code flags to pre-commit config for error codes previously suppressed by inline comments - Fix broken imports after ruff auto-removed re-exports: steam_backlog_enforcer, stockfish_analysis, word_frequency, lichess_bot - Re-add re-exports with __all__ in translator.py, screen_lock.py - Split _process_epc_fc.py (524 lines) into _process_epc_fc.py + _process_fc.py - Fix test failures: keyboard_coop, stockfish_analysis, tag_divider - Add per-file-ignores for PLC0415 (deferred imports) in 7 files - Mark shebang scripts as executable - Add __init__.py for generate_images and repo_explorer packages - Fix codespell, eslint, ruff-format, prettier issues - Update copilot-instructions.md with --no-verify ban
364 lines
13 KiB
Python
364 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._move_analysis import (
|
|
classify_cp_loss,
|
|
fmt_eval,
|
|
score_to_cp,
|
|
)
|
|
from python_pkg.stockfish_analysis.analyze_chess_game import (
|
|
_auto_hash_mb,
|
|
_detect_total_mem_mb,
|
|
_parse_hash_mb,
|
|
_parse_threads,
|
|
extract_pgn_text,
|
|
)
|
|
|
|
|
|
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
|