use std::sync::Mutex;
static CALLS: Mutex<Vec<String>> = Mutex::new(Vec::new());
type AliasedResult = Result<(), String>;
#[allow(dead_code)]
mod single_command {
#[fire::main]
fn hello(
name: String,
age: u32,
nickname: Option<String>,
verbose: bool,
) {
super::CALLS
.lock()
.unwrap()
.push(format!("{name}:{age}:{nickname:?}:{verbose}"));
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
__fire_run_hello(args)
}
}
#[allow(dead_code)]
mod command_group {
#[fire::main]
mod cli {
pub fn say_hello(name: &str) {
super::super::CALLS
.lock()
.unwrap()
.push(format!("hello:{name}"));
}
pub fn bye() {
super::super::CALLS.lock().unwrap().push("bye".to_string());
}
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
cli::__fire_run(args)
}
}
#[allow(dead_code)]
mod async_single_command {
#[fire::main(tokio)]
async fn hello(name: String) {
tokio::task::yield_now().await;
super::CALLS
.lock()
.unwrap()
.push(format!("async-hello:{name}"));
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
__fire_run_hello(args)
}
}
#[allow(dead_code)]
mod async_command_group {
#[fire::main(tokio)]
mod cli {
pub async fn ping(host: String) -> Result<(), String> {
if host == "unreachable" {
return Err("host is unreachable".to_string());
}
tokio::task::yield_now().await;
super::super::CALLS.lock().unwrap().push(format!("ping:{host}"));
Ok(())
}
pub fn version() {
super::super::CALLS.lock().unwrap().push("version".to_string());
}
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
cli::__fire_run(args)
}
}
#[allow(dead_code)]
mod raw_identifier_command {
#[fire::main]
mod cli {
pub fn r#type(r#struct: String) {
super::super::CALLS
.lock()
.unwrap()
.push(format!("type:{}", r#struct));
}
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
cli::__fire_run(args)
}
}
#[allow(dead_code)]
mod aliased_result_command {
#[fire::main]
fn deploy(target: String) -> super::AliasedResult {
if target == "production" {
return Err("production deployments are disabled".to_string());
}
super::CALLS
.lock()
.unwrap()
.push(format!("deploy:{target}"));
Ok(())
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
__fire_run_deploy(args)
}
}
#[allow(dead_code)]
mod other_return_types {
#[fire::main]
mod cli {
#[allow(clippy::unused_unit)]
pub fn unit() -> () {
super::super::CALLS.lock().unwrap().push("unit".to_string());
}
pub fn diverge() -> ! {
panic!("diverged")
}
}
pub(crate) fn run<I, S>(args: I) -> Result<Option<String>, String>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
cli::__fire_run(args)
}
}
fn assert_called(expected: &str) {
let mut calls = CALLS.lock().unwrap();
let index = calls
.iter()
.position(|call| call == expected)
.unwrap_or_else(|| panic!("no call recorded matching '{expected}'"));
calls.remove(index);
}
#[test]
fn function_becomes_cli() {
single_command::run(["--name", "John", "--age=22", "--verbose"]).unwrap();
assert_called("John:22:None:true");
}
#[test]
fn module_functions_become_kebab_case_subcommands() {
command_group::run(["say-hello", "--name", "John"]).unwrap();
assert_called("hello:John");
command_group::run(["bye"]).unwrap();
assert_called("bye");
}
#[test]
fn async_function_becomes_cli() {
async_single_command::run(["--name", "John"]).unwrap();
assert_called("async-hello:John");
}
#[test]
fn async_module_mixes_async_and_sync_commands() {
async_command_group::run(["ping", "--host", "localhost"]).unwrap();
assert_called("ping:localhost");
async_command_group::run(["version"]).unwrap();
assert_called("version");
let error = async_command_group::run(["ping", "--host", "unreachable"]).unwrap_err();
assert_eq!(error, "host is unreachable");
}
#[test]
fn errors_from_an_aliased_result_are_reported() {
aliased_result_command::run(["--target", "staging"]).unwrap();
assert_called("deploy:staging");
let error = aliased_result_command::run(["--target", "production"]).unwrap_err();
assert_eq!(error, "production deployments are disabled");
}
#[test]
fn explicit_unit_and_diverging_commands_are_supported() {
other_return_types::run(["unit"]).unwrap();
assert_called("unit");
}
#[test]
fn errors_are_descriptive() {
let argument_error = single_command::run(["--age", "22"]).unwrap_err();
assert!(argument_error.starts_with("missing required option '--name'"));
assert!(argument_error.contains("Usage:"));
assert!(argument_error.contains("For more information, try '--help'."));
let command_error = command_group::run(["missing"]).unwrap_err();
assert!(command_error.starts_with("unknown command 'missing'"));
assert!(command_error.contains("Usage:"));
assert!(command_error.contains("For more information, try '--help'."));
}
#[test]
fn option_is_not_consumed_as_another_options_value() {
let error = single_command::run(["--name", "--verbose", "--age", "22"]).unwrap_err();
assert!(error.starts_with("option '--name' requires a value"));
assert!(error.contains("Usage:"));
}
#[test]
fn help_flag_is_not_detected_inside_an_option_value() {
let output = single_command::run(["--name=-h", "--age=1"]).unwrap();
assert!(
output.is_none(),
"expected the command to run, got help output"
);
assert_called("-h:1:None:false");
}
#[test]
fn repeated_options_are_rejected() {
let error = single_command::run(["--name", "John", "--name", "Jane", "--age=1"]).unwrap_err();
assert!(error.starts_with("option '--name' is given more than once"));
let error =
single_command::run(["--name=John", "--age=1", "--verbose", "--verbose"]).unwrap_err();
assert!(error.starts_with("flag '--verbose' is given more than once"));
}
#[test]
fn option_like_values_suggest_the_inline_form() {
let error = single_command::run(["--name", "--verbose", "--age", "22"]).unwrap_err();
assert!(error.contains("write '--name=--verbose'"), "got: {error}");
}
#[test]
fn raw_identifiers_drop_their_prefix() {
raw_identifier_command::run(["type", "--struct", "Point"]).unwrap();
assert_called("type:Point");
let help = raw_identifier_command::run(["type", "--help"])
.unwrap()
.unwrap();
assert!(help.contains("--struct <STRUCT>"), "got: {help}");
}
#[cfg(unix)]
#[test]
fn arguments_that_are_not_valid_utf8_are_reported() {
use std::os::unix::ffi::OsStringExt;
let invalid = std::ffi::OsString::from_vec(vec![0x2d, 0x2d, 0xff]);
let error = single_command::run([invalid]).unwrap_err();
assert!(error.contains("is not valid UTF-8"), "got: {error}");
assert!(error.contains("Usage:"));
}
#[test]
fn function_help_uses_signature_and_documentation() {
let help = single_command::run(["--help"]).unwrap().unwrap();
assert!(help.contains("Greet a person."));
assert!(help.contains("Usage:"));
assert!(help.contains("--name <NAME>"));
assert!(help.contains("Person to greet."));
assert!(help.contains("[--nickname <NICKNAME>]"));
assert!(help.contains("[--verbose]"));
assert!(help.contains("-h, --help"));
assert!(
help.contains(" --age <AGE> Person's age.\n"),
"descriptions should be aligned, got: {help}"
);
assert!(
help.ends_with(" -h, --help Print help"),
"got: {help}"
);
}
#[test]
fn command_group_has_root_and_command_help() {
let root = command_group::run(["-h"]).unwrap().unwrap();
assert!(root.contains("Greeting commands."));
assert!(root.contains("say-hello"));
assert!(root.contains("Say hello."));
assert!(
root.contains(" bye Say goodbye.\n"),
"command descriptions should be aligned, got: {root}"
);
let command = command_group::run(["say-hello", "--help"])
.unwrap()
.unwrap();
assert!(command.contains("Usage:"));
assert!(command.contains("say-hello --name <NAME>"));
}