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", "--browser", ]) .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 mut prod = match run_production(8001) { Ok(prod) => prod, err => { snake.kill()?; err?; unreachable!() } }; let res = try_regression(); snake.kill().and(prod.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 = 100; // limit the parallelism rayon::ThreadPoolBuilder::new() .num_threads(std::thread::available_parallelism()?.get() / 3) .build_global() .unwrap(); let won_count = (0..GAMES) .into_par_iter() .map(|_| -> Option { eprint!("."); 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", "http://localhost:8001", "-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 run_production(port: u16) -> Result { let mut snake = Command::new("docker") .args([ "run", "--env", "RUST_LOG=error", "--env", format!("ROCKET_PORT={}", port).as_str(), "--network=host", "-i", "docker.mkaenner.de/snake:latest", ]) .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() }