use std::{ env, net::TcpStream, path::{Path, PathBuf}, process::{Child, Command}, thread::sleep, time::Duration, }; use rayon::iter::{IntoParallelIterator, ParallelIterator}; type DynError = Box; fn main() { if let Err(e) = try_main() { eprintln!("{e}"); std::process::exit(-1) } } fn try_main() -> Result<(), DynError> { let task = env::args().nth(1); match task.as_deref() { Some("local") => local()?, Some("local2") => local2()?, Some("vs_production" | "vs_prod") => vs_production()?, Some("regression") => regression()?, Some("docker") => docker()?, _ => print_help(), } Ok(()) } fn print_help() { eprintln!( "Tasks: local runs the snake on a local game local2 runs the snake twice on a local game vs_production | vs_prod runs the snake against the active server regression runs the snake against the active server multiple times docker package docker image and upload to docker.mkaenner.de/snake " ) } fn local() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local test", "--url", "http://localhost:8000", "-g", "solo", "--browser", ]) .status(); game.and(snake.kill())?; Ok(()) } fn local2() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local test1", "--url", "http://localhost:8000", "--name", "local test2", "--url", "http://localhost:8000", "-g", "duel", "--browser", ]) .status(); game.and(snake.kill())?; Ok(()) } fn vs_production() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local", "--url", "http://localhost:8000", "--name", "production", "--url", "https://snake.mkaenner.de", "-g", "duel", ]) .output(); let game = snake.kill().and(game)?; if !game.status.success() { eprintln!("game output: {}", String::from_utf8(game.stderr)?); eprintln!("game status: {}", game.status); Err("failed to play the game".into()) } else { const WIN_PHRASE: &[u8] = b"local was the winner.\n"; let won = &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()] == WIN_PHRASE; if won { println!("The local snake has won"); } else { println!("The production snake has won"); } Ok(()) } } fn regression() -> Result<(), DynError> { let mut snake = run_snake(8000, Some("error"))?; let res = try_regression(); snake.kill()?; let (won, games) = res?; println!( "\nThe local snake has won {won}/{games} games ({}%)", won * 100 / games ); Ok(()) } fn try_regression() -> Result<(usize, usize), DynError> { const GAMES: usize = 1000; // use only 4 threads so we don't ddos the server rayon::ThreadPoolBuilder::new() .num_threads(4) .build_global() .unwrap(); let won_count = (0..GAMES) .into_par_iter() .map(|_| -> Option { eprint!("."); // let _ = stderr().flush(); let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local", "--url", "http://localhost:8000", "--name", "production", "--url", "https://snake.mkaenner.de", "-g", "duel", ]) .output() .ok()?; if !game.status.success() { eprintln!("game output: {}", String::from_utf8(game.stderr).ok()?); eprintln!("game status: {}", game.status); None } else { const WIN_PHRASE: &[u8] = b"local was the winner.\n"; let won = &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()] == WIN_PHRASE; Some(usize::from(won)) } }) .flatten() .sum(); Ok((won_count, GAMES)) } fn run_snake(port: u16, log: Option<&str>) -> Result { let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); let mut snake = Command::new(cargo); snake .current_dir(project_root()) .env("PORT", port.to_string()); if let Some(log) = log { snake.env("RUST_LOG", log); } let mut snake = snake .args( ["run", "--bin", "battlesnake"] .map(str::to_string) .into_iter() .chain(env::args().skip(2)), ) .spawn()?; loop { if let Some(status) = snake.try_wait()? { Err(format!("{status}"))?; } if TcpStream::connect(("127.0.0.1", port)).is_ok() { break Ok(snake); } sleep(Duration::from_secs(1)); } } fn docker() -> Result<(), DynError> { if !Command::new("docker") .current_dir(project_root()) .args(["build", ".", "-t", "docker.mkaenner.de/snake"]) .status()? .success() { Err("Build of docker image failed")?; } Command::new("docker") .args(["push", "docker.mkaenner.de/snake"]) .status()?; Ok(()) } fn project_root() -> PathBuf { Path::new(&env!("CARGO_MANIFEST_DIR")) .ancestors() .nth(1) .unwrap() .to_path_buf() }