aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorAdam <adammegarules1@gmail.com>2026-08-24 10:30:53 +0200
committerAdam <adammegarules1@gmail.com>2026-08-24 10:30:53 +0200
commit22718aea57fde4e325aea4c0d36d76b0d1609fa7 (patch)
treee01eb8fc249601b95a3ee7da601c62d271d43e5c /cli
parent9a9cbe07f062ce7734ae2b7c6161c0e8ec4b0e4c (diff)
refactor(tests): improving tests by creating testing cli app
Diffstat (limited to 'cli')
-rw-r--r--cli/.gitignore1
-rw-r--r--cli/Cargo.lock107
-rw-r--r--cli/Cargo.toml8
-rw-r--r--cli/src/main.rs233
4 files changed, 349 insertions, 0 deletions
diff --git a/cli/.gitignore b/cli/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/cli/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/cli/Cargo.lock b/cli/Cargo.lock
new file mode 100644
index 0000000..6341cb6
--- /dev/null
+++ b/cli/Cargo.lock
@@ -0,0 +1,107 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "cli"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..9694539
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "cli"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
diff --git a/cli/src/main.rs b/cli/src/main.rs
new file mode 100644
index 0000000..963544c
--- /dev/null
+++ b/cli/src/main.rs
@@ -0,0 +1,233 @@
+use serde::Deserialize;
+use serde_json::Result;
+use std::env;
+use std::io::{BufRead, BufReader, Write};
+use std::process::{ChildStdin, ChildStdout, Command, Stdio};
+use std::string::String;
+
+pub struct ChessEngine {
+ stdin: ChildStdin,
+ reader: BufReader<ChildStdout>,
+}
+
+#[derive(Deserialize)]
+struct PerftResult {
+ ms: i32,
+ result: u64,
+}
+#[derive(Debug)]
+pub struct PerftTest {
+ pub position: &'static str,
+ pub depth: i32,
+ pub expected: u64,
+}
+
+pub const PERFT_TESTS: &[PerftTest; 17] = &[
+ PerftTest {
+ position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ depth: 1,
+ expected: 20,
+ },
+ PerftTest {
+ position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ depth: 2,
+ expected: 400,
+ },
+ PerftTest {
+ position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ depth: 3,
+ expected: 8902,
+ },
+ PerftTest {
+ position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ depth: 4,
+ expected: 197281,
+ },
+ PerftTest {
+ position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ depth: 5,
+ expected: 4865609,
+ },
+ // kiwipete
+ PerftTest {
+ position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ depth: 1,
+ expected: 48,
+ },
+ PerftTest {
+ position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ depth: 2,
+ expected: 2039,
+ },
+ PerftTest {
+ position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ depth: 3,
+ expected: 97862,
+ },
+ PerftTest {
+ position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ depth: 4,
+ expected: 4085603,
+ },
+ // other ones
+ PerftTest {
+ position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
+ depth: 1,
+ expected: 14,
+ },
+ PerftTest {
+ position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
+ depth: 2,
+ expected: 191,
+ },
+ PerftTest {
+ position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
+ depth: 3,
+ expected: 2812,
+ },
+ PerftTest {
+ position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
+ depth: 4,
+ expected: 43238,
+ },
+ // other position
+ PerftTest {
+ position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
+ depth: 1,
+ expected: 6,
+ },
+ PerftTest {
+ position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
+ depth: 2,
+ expected: 264,
+ },
+ PerftTest {
+ position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
+ depth: 3,
+ expected: 9467,
+ },
+ PerftTest {
+ position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
+ depth: 4,
+ expected: 422333,
+ },
+];
+
+impl ChessEngine {
+ /// Spawns the engine process and initializes UCI mode
+ pub fn new(path_to_engine: &str) -> Self {
+ let mut child = Command::new(path_to_engine)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .expect("Failed to start chess engine");
+
+ let stdin = child.stdin.take().expect("Failed to open stdin");
+ let stdout = child.stdout.take().expect("Failed to open stdout");
+ let reader = BufReader::new(stdout);
+
+ let mut engine = Self { stdin, reader };
+
+ // Boot up UCI mode
+ engine.send_command("uci");
+ engine.wait_for_response("uciok");
+
+ engine
+ }
+
+ /// Sends a clean string command to the engine.
+ /// Explicitly appends a newline and flushes the stream.
+ pub fn send_command(&mut self, cmd: &str) {
+ let formatted = format!("{}\n", cmd);
+ self.stdin
+ .write_all(formatted.as_bytes())
+ .expect("Failed to write to engine");
+
+ self.stdin.flush().expect("Failed to flush stdin");
+ }
+
+ /// Blocks and reads lines until a specific keyword signature is found
+ pub fn wait_for_response(&mut self, expected_keyword: &str) -> Vec<String> {
+ let mut lines = Vec::new();
+ let mut line = String::new();
+
+ while self.reader.read_line(&mut line).is_ok() {
+ let clean_line = line.trim().to_string();
+ if clean_line.starts_with("info") {
+ println!("{}", clean_line)
+ }
+ lines.push(clean_line.clone());
+
+ // Clear buffer for the next read loop
+ line.clear();
+
+ if clean_line.contains(expected_keyword) {
+ break;
+ }
+ }
+ lines
+ }
+ pub fn read_line(&mut self) -> String {
+ let mut line = String::new();
+
+ let _ = self.reader.read_line(&mut line);
+
+ let clean_line = line.trim().to_string();
+ if clean_line.starts_with("info") {
+ println!("{}", clean_line)
+ }
+ clean_line
+ }
+}
+
+fn test_perft(test: &PerftTest, engine: &mut ChessEngine) {
+ let cmd = "position fen ".to_string() + &test.position;
+ engine.send_command(&cmd);
+
+ let cmd = "perft ".to_string() + &test.depth.to_string();
+ engine.send_command(&cmd);
+ let line = engine.read_line();
+ let result = parse_perft_result(&line);
+ match result {
+ Err(err) => {
+ println!("error: {}", err);
+ }
+ Ok(perft) => {
+ if perft.result == test.expected {
+ println!(
+ "passed (Depth {}): {} nodes in {}ms",
+ test.depth, perft.result, perft.ms
+ );
+ } else {
+ println!(
+ "FAILED Depth: {}, Expected: {}, got: {}, position: {}",
+ test.depth, test.expected, perft.result, test.position,
+ );
+ std::process::exit(1);
+ }
+ }
+ }
+}
+fn parse_perft_result(raw: &str) -> Result<PerftResult> {
+ serde_json::from_str(raw)
+}
+
+fn main() {
+ let args: Vec<String> = env::args().collect();
+
+ if args.len() != 2 {
+ println!("usage: ./cli <path to test executable>");
+ return;
+ }
+
+ let engine_path = &args[1];
+ let mut engine = ChessEngine::new(engine_path);
+
+ println!("Engine running");
+
+ engine.send_command("isready");
+ engine.wait_for_response("readyok");
+ for test in PERFT_TESTS.iter() {
+ test_perft(test, &mut engine);
+ }
+}