use clap::{Args, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Subcommand, Debug, Clone)]
pub enum SimCommand {
Generate(SimGenerateArgs),
Run(SimRunArgs),
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SimBackend {
#[default]
Mujoco,
}
#[derive(Args, Debug, Clone)]
pub struct SimGenerateArgs {
#[arg(long, value_enum, default_value_t = SimBackend::Mujoco)]
pub backend: SimBackend,
#[arg(long, default_value = "rover")]
pub model: String,
#[arg(long)]
pub env: Option<String>,
#[arg(long)]
pub out: PathBuf,
#[arg(long)]
pub assets_path: Option<PathBuf>,
}
#[derive(Args, Debug, Clone)]
pub struct SimRunArgs {
#[arg(long, value_enum, default_value_t = SimBackend::Mujoco)]
pub backend: SimBackend,
#[arg(long, default_value = "rover")]
pub model: String,
#[arg(long, default_value = "basic_arena")]
pub env: String,
#[arg(long, default_value_t = 100)]
pub steps: u32,
#[arg(long)]
pub assets_path: Option<PathBuf>,
}
#[cfg(test)]
mod tests {
use crate::types::{Cli, Commands};
use clap::Parser;
fn parse_sim(args: &[&str]) -> super::SimCommand {
let argv: Vec<&str> = std::iter::once("mecha10").chain(args.iter().copied()).collect();
match Cli::try_parse_from(argv) {
Ok(cli) => match cli.command {
Commands::Sim { command } => command,
_ => panic!("expected Commands::Sim"),
},
Err(e) => panic!("expected successful parse, got: {e}"),
}
}
#[test]
fn generate_requires_out() {
let err = Cli::try_parse_from(["mecha10", "sim", "generate"])
.map(|_| ())
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn generate_defaults_backend_mujoco_and_model_rover_with_no_env() {
let super::SimCommand::Generate(args) = parse_sim(&["sim", "generate", "--out", "/tmp/rover.xml"]) else {
panic!("expected SimCommand::Generate");
};
assert_eq!(args.backend, super::SimBackend::Mujoco);
assert_eq!(args.model, "rover");
assert_eq!(args.env, None);
assert_eq!(args.out, std::path::PathBuf::from("/tmp/rover.xml"));
assert_eq!(args.assets_path, None);
}
#[test]
fn generate_parses_all_optional_flags() {
let super::SimCommand::Generate(args) = parse_sim(&[
"sim",
"generate",
"--model",
"arm",
"--env",
"basic_arena",
"--out",
"/tmp/arm.xml",
"--assets-path",
"/tmp/assets",
]) else {
panic!("expected SimCommand::Generate");
};
assert_eq!(args.model, "arm");
assert_eq!(args.env, Some("basic_arena".to_string()));
assert_eq!(args.out, std::path::PathBuf::from("/tmp/arm.xml"));
assert_eq!(args.assets_path, Some(std::path::PathBuf::from("/tmp/assets")));
}
#[test]
fn generate_rejects_unknown_backend() {
let err = Cli::try_parse_from([
"mecha10",
"sim",
"generate",
"--out",
"/tmp/rover.xml",
"--backend",
"godot",
])
.map(|_| ())
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn run_defaults_env_basic_arena_and_steps_100_with_no_out_required() {
let super::SimCommand::Run(args) = parse_sim(&["sim", "run"]) else {
panic!("expected SimCommand::Run");
};
assert_eq!(args.backend, super::SimBackend::Mujoco);
assert_eq!(args.model, "rover");
assert_eq!(args.env, "basic_arena");
assert_eq!(args.steps, 100);
assert_eq!(args.assets_path, None);
}
#[test]
fn run_parses_all_optional_flags() {
let super::SimCommand::Run(args) = parse_sim(&[
"sim",
"run",
"--model",
"arm",
"--env",
"warehouse",
"--steps",
"250",
"--assets-path",
"/tmp/assets",
]) else {
panic!("expected SimCommand::Run");
};
assert_eq!(args.model, "arm");
assert_eq!(args.env, "warehouse");
assert_eq!(args.steps, 250);
assert_eq!(args.assets_path, Some(std::path::PathBuf::from("/tmp/assets")));
}
#[test]
fn run_rejects_non_numeric_steps() {
let err = Cli::try_parse_from(["mecha10", "sim", "run", "--steps", "not-a-number"])
.map(|_| ())
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
}
}