From 5b5986b09503d9ec74d409b8c6f22fe538c48b48 Mon Sep 17 00:00:00 2001 From: rn5f107s2 Date: Sun, 15 Feb 2026 22:35:56 +0100 Subject: [PATCH] Add en passant santization tests to the CI and pos is ok closes https://github.com/official-stockfish/Stockfish/pull/6615 No functional change --- src/position.cpp | 17 ++++++++ tests/instrumented.py | 91 ++++++++++++++++++++++++++++++++++++++++++- tests/testing.py | 19 +++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/position.cpp b/src/position.cpp index 22784dd01..350a207fa 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -1515,6 +1515,23 @@ bool Position::pos_is_ok() const { if ((pieces(PAWN) & (Rank1BB | Rank8BB)) || pieceCount[W_PAWN] > 8 || pieceCount[B_PAWN] > 8) assert(0 && "pos_is_ok: Pawns"); + + if (ep_square() != SQ_NONE) + { + Square ksq = square(sideToMove); + + Bitboard captured = (ep_square() + pawn_push(~sideToMove)) & pieces(~sideToMove, PAWN); + Bitboard pawns = attacks_bb(ep_square(), ~sideToMove) & pieces(sideToMove, PAWN); + Bitboard potentialCheckers = pieces(~sideToMove) ^ captured; + + if (!captured || !pawns + || ((attackers_to(ksq, pieces() ^ captured ^ ep_square() ^ lsb(pawns)) + & potentialCheckers) + && (attackers_to(ksq, pieces() ^ captured ^ ep_square() ^ msb(pawns)) + & potentialCheckers))) + assert(0 && "pos_is_ok: En passant square"); + } + if ((pieces(WHITE) & pieces(BLACK)) || (pieces(WHITE) | pieces(BLACK)) != pieces() || popcount(pieces(WHITE)) > 16 || popcount(pieces(BLACK)) > 16) assert(0 && "pos_is_ok: Bitboards"); diff --git a/tests/instrumented.py b/tests/instrumented.py index 80831ce3f..dd84845ba 100644 --- a/tests/instrumented.py +++ b/tests/instrumented.py @@ -4,6 +4,7 @@ import sys import subprocess import pathlib import os +import fnmatch from testing import ( EPD, @@ -474,6 +475,94 @@ class TestSyzygy(metaclass=OrderedClassMembers): self.stockfish.check_output(check_output) self.stockfish.expect("bestmove *") +class TestEnPassantSanitization(metaclass=OrderedClassMembers): + def beforeAll(self): + self.stockfish = Stockfish() + + def afterAll(self): + self.stockfish.quit() + assert self.stockfish.close() == 0 + + def afterEach(self): + assert postfix_check(self.stockfish.get_output()) == True + self.stockfish.clear_output() + + def test_position_1(self): + self.stockfish.send_command("position fen rnbqkbnr/ppp1p1pp/5p2/3pP3/8/8/PPPP1PPP/RNBQKBNR w kq d6 0 3") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*rnbqkbnr/ppp1p1pp/5p2/3pP3/8/8/PPPP1PPP/RNBQKBNR w kq d6 0 3*") + + def test_position_2(self): + self.stockfish.send_command("position fen k7/8/8/1pP5/2K5/8/8/8 w - b6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k7/8/8/1pP5/2K5/8/8/8 w - b6 0 1*") + + def test_position_3(self): + self.stockfish.send_command("position fen k1r5/8/8/1pP5/2K5/8/8/8 w - b6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k1r5/8/8/1pP5/2K5/8/8/8 w - - 0 1*") + + def test_position_4(self): + self.stockfish.send_command("position fen k1r5/8/8/1pP5/8/2K5/8/8 w - b6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k1r5/8/8/1pP5/8/2K5/8/8 w - - 0 1*") + + def test_position_5(self): + self.stockfish.send_command("position fen k1r5/8/8/PpP5/8/2K5/8/8 w - b6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k1r5/8/8/PpP5/8/2K5/8/8 w - b6 0 1*") + + def test_position_6(self): + self.stockfish.send_command("position fen k1r5/8/8/PpP5/2K5/8/8/8 w - b6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k1r5/8/8/PpP5/2K5/8/8/8 w - b6 0 1*") + + def test_position_7(self): + self.stockfish.send_command("position fen k7/4b3/8/PpP5/1K6/8/8/8 w - b6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k7/4b3/8/PpP5/1K6/8/8/8 w - b6 0 1*") + + def test_position_8(self): + self.stockfish.send_command("position fen k7/b5b1/8/2PpP3/3K4/8/8/8 w - d6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k7/b5b1/8/2PpP3/3K4/8/8/8 w - - 0 1*") + + def test_position_9(self): + self.stockfish.send_command("position fen k7/8/8/r2pPK2/8/8/8/8 w - d6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k7/8/8/r2pPK2/8/8/8/8 w - - 0 1*") + + def test_position_10(self): + self.stockfish.send_command("position fen k7/8/8/r1PpPK2/8/8/8/8 w - d6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*k7/8/8/r1PpPK2/8/8/8/8 w - d6 0 1*") + + def test_position_11(self): + self.stockfish.send_command("position fen kb6/8/8/3pP3/5K2/8/8/8 w - d6 0 1") + self.stockfish.send_command("d") + + self.stockfish.expect_for_line_matching("Fen*", "*kb6/8/8/3pP3/5K2/8/8/8 w - d6 0 1*") + + def test_position_find_draw(self): + self.stockfish.send_command("position fen q4kb1/3Q2nq/8/r3PpK1/2n5/7q/8/q7 w - f6 0 1 moves d7c8 f8f7 c8d7 f7f8 d7d8 f8f7") + self.stockfish.send_command("go nodes 10000") + + def check_output(output): + if fnmatch.fnmatch(output, "* score cp 0 * pv d8d7*"): + return True + + self.stockfish.check_output(check_output) + self.stockfish.expect("bestmove d8d7*") def parse_args(): parser = argparse.ArgumentParser(description="Run Stockfish with testing options") @@ -508,7 +597,7 @@ if __name__ == "__main__": framework = MiniTestFramework() # Each test suite will be run inside a temporary directory - framework.run([TestCLI, TestInteractive, TestSyzygy]) + framework.run([TestCLI, TestInteractive, TestSyzygy, TestEnPassantSanitization]) EPD.delete_bench_epd() TSAN.unset_tsan_option() diff --git a/tests/testing.py b/tests/testing.py index 3a4b537e9..360ecadcd 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -126,6 +126,11 @@ class TimeoutException(Exception): self.message = message self.timeout = timeout +class UnexpectedOutputException(Exception): + def __init__(self, actual: str, expected: str): + self.actual = actual + self.expected = expected + def timeout_decorator(timeout: float): def decorator(func): @@ -230,6 +235,11 @@ class MiniTestFramework: f" {method} (hit execution limit of {e.timeout} seconds)" ) + if isinstance(e, UnexpectedOutputException): + self.print_failure( + f" {method} encountered unexpeted output: \"{e.actual}\" when output matching \"{e.expected}\" was expected" + ) + if isinstance(e, AssertionError): self.__handle_assertion_error(t0, method) @@ -371,6 +381,15 @@ class Stockfish: if callback(line) == True: return + @timeout_decorator(MAX_TIMEOUT) + def expect_for_line_matching(self, line_match: str, expected: str): + for line in self.readline(): + if fnmatch.fnmatch(line, line_match): + if fnmatch.fnmatch(line, expected): + break + else: + raise UnexpectedOutputException(line, expected) + def readline(self): if not self.process: raise RuntimeError("Stockfish process is not started")