battlesnake/xtask/src/main.rs

291 lines
7.4 KiB
Rust

use std::{
env,
net::TcpStream,
path::{Path, PathBuf},
process::{Child, Command},
thread::sleep,
time::Duration,
};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
type DynError = Box<dyn std::error::Error>;
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<usize> {
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<Child, DynError> {
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<Child, DynError> {
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()
}