codegame 0.7.0

CodeGame framework
Documentation
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::Command;

use super::*;

macro_rules! project_file {
    ($options: expr, $path:expr $(,)?) => {{
        use heck::*;
        include_str!($path)
            .replace("project_name", &$options.name.to_snake_case())
            .replace("ProjectName", &$options.name.to_camel_case())
            .as_str()
    }};
}

#[derive(Debug)]
pub struct Options<'a> {
    pub name: &'a str,
    pub target_dir: &'a Path,
    pub version: &'a str,
}

pub trait ClientGen<G: Game> {
    const NAME: &'static str;
    const RUNNABLE: bool;
    type GenOptions: Debug + Default;
    fn gen(options: &Options, gen_options: Self::GenOptions) -> anyhow::Result<()>;
    fn build_local(options: &Options) -> anyhow::Result<()>;
    fn run_local(options: &Options) -> anyhow::Result<Command>;
}

fn write_file<P: AsRef<Path>>(path: P, content: &str) -> anyhow::Result<()> {
    if let Some(dir) = path.as_ref().parent() {
        std::fs::create_dir_all(dir)?;
    }
    File::create(path)?.write_all(content.as_bytes())?;
    Ok(())
}

macro_rules! all_langs {
    ($invoke:path) => {
        $invoke!(cpp);
        $invoke!(csharp);
        $invoke!(dlang);
        $invoke!(fsharp);
        $invoke!(go);
        $invoke!(java);
        $invoke!(javascript);
        $invoke!(kotlin);
        $invoke!(python);
        $invoke!(ruby);
        $invoke!(rust);
        $invoke!(scala);
        $invoke!(markdown);
    };
}

macro_rules! lang_mod {
    ($lang:ident) => {
        mod $lang;
    };
}
all_langs!(lang_mod);

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TestMode {
    Gen,
    Build,
    Run,
    BuildDocker,
    RunDocker,
}

impl std::str::FromStr for TestMode {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> anyhow::Result<Self> {
        Ok(match s {
            "gen" => Self::Gen,
            "build" => Self::Build,
            "run" => Self::Run,
            "build-docker" => Self::BuildDocker,
            "run-docker" => Self::RunDocker,
            _ => anyhow::bail!("Only gen | build[-docker] | run[-docker]"),
        })
    }
}

#[derive(Debug)]
pub struct TestOptions {
    pub clean: bool,
    pub mode: TestMode,
    pub host_from_docker: Option<String>,
}

pub fn test<G, CG>(
    options: &Options,
    gen_options: CG::GenOptions,
    extra_files: &HashMap<&str, &str>,
    test_options: &TestOptions,
) -> anyhow::Result<()>
where
    G: Game,
    CG: ClientGen<G>,
    G::Options: Default,
    G::Action: Default,
    G::DebugState: Default,
{
    info!("Generating {} with options {:#?}", CG::NAME, gen_options);
    CG::gen(options, gen_options)?;
    for (path, contents) in extra_files {
        std::fs::write(options.target_dir.join(path), contents)?;
    }

    if CG::RUNNABLE && matches!(test_options.mode, TestMode::Build | TestMode::Run) {
        info!("Building...");
        CG::build_local(options)?;
    }

    let docker_image = format!("codegame-test:{}", CG::NAME);

    if CG::RUNNABLE
        && matches!(
            test_options.mode,
            TestMode::BuildDocker | TestMode::RunDocker
        )
    {
        info!("Building docker...");
        command("docker")
            .current_dir(options.target_dir)
            .arg("build")
            .arg("-t")
            .arg(&docker_image)
            .arg(".")
            .run()?;
        info!("Compiling base package...");
        let mut child = command("docker build -t codegame-test -")
            .stdin(std::process::Stdio::piped())
            .spawn()?;
        child.stdin.take().unwrap().write_all(
            format!(
                "FROM {}\nRUN mkdir /output && sh compile.sh base",
                docker_image
            )
            .as_bytes(),
        )?;
        let status = child.wait()?;
        if !status.success() {
            anyhow::bail!("Process exited with {}", status);
        }
    }

    if CG::RUNNABLE && matches!(test_options.mode, TestMode::Run | TestMode::RunDocker) {
        info!("Running...");
        const PORT: u16 = 31005;
        const TOKEN: &str = "CODEGAME_TOKEN";
        let mut command = match test_options.mode {
            TestMode::Run => {
                let mut command = CG::run_local(options)?;
                command.arg("127.0.0.1").arg(PORT.to_string()).arg(TOKEN);
                command
            }
            TestMode::RunDocker => {
                let mut command = command("docker");
                command
                    .arg("run")
                    .arg("--rm")
                    .arg("--name")
                    .arg("codegame-test")
                    .arg("--network")
                    .arg("host")
                    .arg("codegame-test")
                    .arg("sh")
                    .arg("run.sh");
                command
                    .arg(
                        test_options
                            .host_from_docker
                            .as_ref()
                            .map(|host| host.as_str())
                            .unwrap_or("localhost"),
                    )
                    .arg(PORT.to_string())
                    .arg(TOKEN);
                command.current_dir(options.target_dir);
                command
            }
            _ => unreachable!(),
        };
        let client_player = TcpPlayer::<G>::new(TcpPlayerOptions {
            host: if test_options.host_from_docker.is_some() {
                Some("0.0.0.0".to_owned())
            } else {
                None
            },
            port: PORT,
            accept_timeout: Some(10.0),
            timeout: Some(10.0),
            token: Some(TOKEN.to_owned()),
        });
        let client_thread = std::thread::spawn(move || {
            let start_time = std::time::Instant::now();
            command.run().expect("Running client failed");
            std::time::Instant::now().duration_since(start_time)
        });
        let players = vec![
            Box::new(futures::executor::block_on(client_player)?) as Box<_>,
            Box::new(EmptyPlayer) as Box<_>,
        ];
        let processor = GameProcessor::new(None, default(), players);
        processor.run(Some(&DebugInterface {
            debug_command_handler: Box::new(|_player_index, _global, _command| {}),
            debug_state: Box::new(|_player_index| default()),
        }));
        match client_thread.join() {
            Ok(duration) => info!("Client running time: {} ms", duration.as_millis()),
            Err(_e) => anyhow::bail!("Running client failed"),
        }
    }
    info!("Success");
    Ok(())
}

pub fn test_all<G>(
    options: &Options,
    extra_files: &HashMap<&str, HashMap<&str, &str>>,
    language_filter: Option<HashSet<&str>>,
    gen_options: &HashMap<String, serde_json::Value>,
    test_options: &TestOptions,
) -> anyhow::Result<()>
where
    G: Game,
    G::Options: Default,
    G::Action: Default,
    G::DebugState: Default,
{
    macro_rules! test {
        ($lang:ident) => {{
            type CG = trans_gen::GeneratorImpl<$lang::Generator>;
            if language_filter
                .as_ref()
                .map_or(true, |filter| filter.contains(<CG as ClientGen<G>>::NAME))
            {
                let empty_extra_files = HashMap::new();
                let language_target_dir = options.target_dir.join(<CG as ClientGen<G>>::NAME);
                test::<G, CG>(
                    &if language_filter
                        .as_ref()
                        .map_or(false, |filter| filter.len() == 1)
                    {
                        Options { ..*options }
                    } else {
                        Options {
                            target_dir: language_target_dir.as_ref(),
                            ..*options
                        }
                    },
                    gen_options
                        .get(<CG as ClientGen<G>>::NAME)
                        .map(|value| {
                            serde_json::from_value(value.clone())
                                .expect("Failed to parse gen options")
                        })
                        .unwrap_or_default(),
                    if let Some(extra_files) = extra_files.get(<CG as ClientGen<G>>::NAME) {
                        extra_files
                    } else {
                        &empty_extra_files
                    },
                    test_options,
                )?;
            }
        }};
    }
    if test_options.clean && options.target_dir.exists() {
        std::fs::remove_dir_all(options.target_dir)?;
    }
    all_langs!(test);
    Ok(())
}

trait CommandExt {
    fn run(&mut self) -> anyhow::Result<()>;
}

impl CommandExt for Command {
    fn run(&mut self) -> anyhow::Result<()> {
        let status = self.status()?;
        if !status.success() {
            anyhow::bail!("Process exited with {}", status);
        }
        Ok(())
    }
}

fn command(cmd: &str) -> Command {
    let mut parts = cmd.split_whitespace();
    let mut command = if cfg!(windows) {
        let mut command = Command::new("cmd");
        command.arg("/C").arg(parts.next().unwrap());
        command
    } else {
        Command::new(parts.next().unwrap())
    };
    for part in parts {
        command.arg(part);
    }
    command
}