Spaces:
Runtime error
Runtime error
| """Test the functions that get the external moves.""" | |
| import pytest | |
| import backoff | |
| import requests | |
| import yaml | |
| import os | |
| import chess | |
| import logging | |
| import chess.engine | |
| from datetime import timedelta | |
| from copy import deepcopy | |
| from requests.exceptions import ConnectionError as RequestsConnectionError, HTTPError, ReadTimeout, RequestException | |
| from http.client import RemoteDisconnected | |
| from lib.lichess_types import OnlineType, GameEventType | |
| from typing import cast | |
| from lib.lichess import is_final, backoff_handler, Lichess | |
| from lib.config import Configuration, insert_default_values | |
| from lib.model import Game | |
| from lib.engine_wrapper import get_online_move, get_book_move, is_op1_position | |
| class MockLichess(Lichess): | |
| """A modified Lichess class for communication with external move sources.""" | |
| def __init__(self) -> None: | |
| """Initialize only self.other_session and not self.session.""" | |
| self.max_retries = 3 | |
| self.other_session = requests.Session() | |
| def online_book_get(self, path: str, params: dict[str, str | int] | None = None, *, | |
| stream: bool = False, authenticated: bool = False) -> OnlineType: | |
| """ | |
| Get an external move from online sources (chessdb or lichess.org). | |
| Ignore authentication for tests. | |
| """ | |
| del authenticated | |
| def online_book_get() -> OnlineType: | |
| json_response: OnlineType = self.other_session.get(path, timeout=2, params=params, stream=stream).json() | |
| return json_response | |
| return online_book_get() | |
| def is_website_up(self, url: str) -> bool: | |
| """Check if a website is up.""" | |
| try: | |
| self.other_session.get(url, timeout=2) | |
| return True | |
| except RequestException: | |
| return False | |
| def get_configs() -> tuple[Configuration, Configuration, Configuration, Configuration]: | |
| """Create the configs used for the tests.""" | |
| with open("./config.yml.default") as file: | |
| CONFIG = yaml.safe_load(file) | |
| insert_default_values(CONFIG) | |
| CONFIG["engine"]["online_moves"]["lichess_cloud_analysis"]["enabled"] = True | |
| CONFIG["engine"]["online_moves"]["online_egtb"]["enabled"] = True | |
| CONFIG["engine"]["draw_or_resign"]["resign_enabled"] = True | |
| CONFIG["engine"]["polyglot"]["enabled"] = True | |
| CONFIG["engine"]["polyglot"]["book"]["standard"] = ["TEMP/gm2001.bin"] | |
| engine_cfg = Configuration(CONFIG).engine | |
| CONFIG_2 = deepcopy(CONFIG) | |
| CONFIG_2["engine"]["online_moves"]["chessdb_book"]["enabled"] = True | |
| CONFIG_2["engine"]["online_moves"]["online_egtb"]["source"] = "chessdb" | |
| engine_cfg_2 = Configuration(CONFIG_2).engine | |
| return engine_cfg.online_moves, engine_cfg_2.online_moves, engine_cfg.draw_or_resign, engine_cfg.polyglot | |
| def get_game() -> Game: | |
| """Create a model.Game to be used in the tests.""" | |
| game_event: GameEventType = {"id": "zzzzzzzz", | |
| "variant": {"key": "standard", | |
| "name": "Standard", | |
| "short": "Std"}, | |
| "clock": {"initial": 60000, | |
| "increment": 2000}, | |
| "speed": "bullet", | |
| "perf": {"name": "Bullet"}, | |
| "rated": True, | |
| "createdAt": 1600000000000, | |
| "white": {"id": "bo", | |
| "name": "bo", | |
| "title": "BOT", | |
| "rating": 3000}, | |
| "black": {"id": "b", | |
| "name": "b", | |
| "title": "BOT", | |
| "rating": 3000, | |
| "provisional": True}, | |
| "initialFen": "startpos", | |
| "type": "gameFull", | |
| "state": {"type": "gameState", | |
| "moves": "", | |
| "wtime": 1000000, | |
| "btime": 1000000, | |
| "winc": 2000, | |
| "binc": 2000, | |
| "status": "started"}} | |
| return Game(game_event, "b", "https://lichess.org", timedelta(seconds=60)) | |
| def download_opening_book() -> None: | |
| """Download gm2001.bin.""" | |
| if os.path.exists("./TEMP/gm2001.bin"): | |
| return | |
| os.makedirs("TEMP", exist_ok=True) | |
| response = requests.get("https://github.com/gmcheems-org/free-opening-books/raw/main/books/bin/gm2001.bin", | |
| allow_redirects=True, timeout=60) | |
| if response.status_code != 200: | |
| pytest.xfail("Could not download opening book.") | |
| with open("./TEMP/gm2001.bin", "wb") as file: | |
| file.write(response.content) | |
| def get_online_move_wrapper(li: Lichess, board: chess.Board, game: Game, online_moves_cfg: Configuration, | |
| draw_or_resign_cfg: Configuration, *, expect_none: bool = False) -> chess.engine.PlayResult: | |
| """Wrap `lib.engine_wrapper.get_online_move` so that it only returns a PlayResult type.""" | |
| online_move = get_online_move(li, board, game, online_moves_cfg, draw_or_resign_cfg) | |
| online_move = cast(chess.engine.PlayResult, online_move) | |
| if not expect_none and online_move.move is None: | |
| pytest.xfail("Could not contact external move source.") | |
| return online_move | |
| class TestExternalMoves: | |
| """Test that the code for external moves works properly.""" | |
| li = MockLichess() | |
| game = get_game() | |
| online_cfg, online_cfg_2, draw_or_resign_cfg, polyglot_cfg = get_configs() | |
| starting_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" | |
| opening_fen = "rn1q1rk1/pbp1bpp1/1p2pn1p/3p4/2PP3B/2N1PN2/PP2BPPP/R2QK2R w KQ - 2 9" | |
| middlegame_fen = "8/5p2/1n1p1nk1/1p1Pp1p1/1Pp1P1Pp/r1P2B1P/2RNKP2/8 w - - 0 31" | |
| endgame_wdl2_fen = "2k5/4n2Q/5N2/8/8/8/1r6/2K5 b - - 0 123" | |
| endgame_wdl1_fen = "6N1/3n4/3k1b2/8/8/7Q/1r6/5K2 b - - 6 9" | |
| endgame_wdl0_fen = "6N1/3n4/3k1b2/8/8/7Q/5K2/1r6 b - - 8 10" | |
| endgame_op1_fen = "r7/p1b1k3/8/8/P7/8/8/K2R1R2 w - - 1 2" | |
| def test_lichess_cloud_analysis(self) -> None: | |
| """Test lichess_cloud_analysis.""" | |
| if not self.li.is_website_up("https://lichess.org/api/cloud-eval"): | |
| pytest.xfail("Lichess cloud eval is down.") | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.starting_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg).move is not None | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.opening_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg).move is not None | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.middlegame_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg, | |
| expect_none=True).move is None | |
| def test_chessdb_book(self) -> None: | |
| """Test chessdb_book.""" | |
| if not self.li.is_website_up("https://www.chessdb.cn/cdb.php"): | |
| pytest.xfail("ChessDB is down.") | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.starting_fen), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg).move is not None | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.opening_fen), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg).move is not None | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.middlegame_fen), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg, | |
| expect_none=True).move is None | |
| def test_online_egtb_with_lichess(self) -> None: | |
| """Test online_egtb with lichess.""" | |
| if not self.li.is_website_up("https://tablebase.lichess.ovh/standard"): | |
| pytest.xfail("Lichess tablebase is down.") | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl2_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg).resigned | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl0_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg).draw_offered | |
| wdl1_move = get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl1_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg) | |
| assert not wdl1_move.resigned and not wdl1_move.draw_offered | |
| op1_move = get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_op1_fen), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg) | |
| assert op1_move.move == chess.Move.from_uci("f1e1") | |
| # Test with reversed colors. | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl2_fen).mirror(), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg).resigned | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl0_fen).mirror(), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg).draw_offered | |
| wdl1_move = get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl1_fen).mirror(), | |
| self.game, | |
| self.online_cfg, | |
| self.draw_or_resign_cfg) | |
| assert not wdl1_move.resigned and not wdl1_move.draw_offered | |
| def test_online_egtb_with_chessdb(self) -> None: | |
| """Test online_egtb with chessdb.""" | |
| if not self.li.is_website_up("https://www.chessdb.cn/cdb.php"): | |
| pytest.xfail("ChessDB is down.") | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl2_fen), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg).resigned | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl0_fen), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg).draw_offered | |
| wdl1_move = get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl1_fen), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg) | |
| assert not wdl1_move.resigned and not wdl1_move.draw_offered | |
| # Test with reversed colors. | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl2_fen).mirror(), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg).resigned | |
| assert get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl0_fen).mirror(), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg).draw_offered | |
| wdl1_move = get_online_move_wrapper(self.li, | |
| chess.Board(self.endgame_wdl1_fen).mirror(), | |
| self.game, | |
| self.online_cfg_2, | |
| self.draw_or_resign_cfg) | |
| assert not wdl1_move.resigned and not wdl1_move.draw_offered | |
| def test_opening_book(self) -> None: | |
| """Test opening book.""" | |
| download_opening_book() | |
| assert get_book_move(chess.Board(self.opening_fen), self.game, self.polyglot_cfg).move == chess.Move.from_uci("h4f6") | |
| class TestExternalMoveHelpers: | |
| """Test the helper functions for external moves.""" | |
| def test_op1_positions(self) -> None: | |
| """Test that a position is op1.""" | |
| fens_op1 = ["r7/p1b1k3/8/8/P7/8/8/K2R1R2 w - - 1 2", "3qk3/1n1pp3/8/8/8/8/4P2P/4K3 w - - 0 1"] | |
| fens_not_op1 = ["r3k3/2b5/8/p7/P7/8/8/K2R1R2 w q - 0 2", "3qk3/1n1pp1n1/8/8/8/8/4P3/4K3 w - - 0 1"] | |
| for fen in fens_op1: | |
| assert is_op1_position(chess.Board(fen)) | |
| for fen in fens_not_op1: | |
| assert not is_op1_position(chess.Board(fen)) | |