mod args;
mod commands;
mod format;
mod help;
mod load;
mod materials;
mod outcome;
mod stone;
#[cfg(test)]
mod testing;
pub use outcome::{
EXIT_DESIGN, EXIT_IO, EXIT_OK, EXIT_PROBLEM, EXIT_RENDER, EXIT_USAGE, OutFile, Outcome,
};
use outcome::CliError;
use std::{fmt::Write as _, io::IsTerminal as _};
fn execute(args: &[String], progress: &mut dyn std::io::Write, terminal: bool) -> Outcome {
let command = match args::parse(args) {
Ok(command) => command,
Err(message) => {
let mut outcome = Outcome::failure(&CliError::usage(message));
outcome.stderr.push_str("try: indicatrix-cli --help\n");
return outcome;
}
};
commands::dispatch_streaming(&command, progress, terminal)
.unwrap_or_else(|error| Outcome::failure(&error))
}
#[must_use]
pub fn run_command_line(args: &[String]) -> Outcome {
let mut buffer: Vec<u8> = Vec::new();
let mut outcome = execute(args, &mut buffer, false);
if !buffer.is_empty() {
outcome
.stderr
.insert_str(0, &String::from_utf8_lossy(&buffer));
}
outcome
}
#[must_use]
pub fn run(args: &[String]) -> Outcome {
let mut stderr = std::io::stderr();
let terminal = stderr.is_terminal();
let mut outcome = execute(args, &mut stderr, terminal);
if outcome.exit != EXIT_OK && outcome.exit != EXIT_PROBLEM {
return outcome;
}
for file in &outcome.files {
match std::fs::write(&file.path, &file.text) {
Ok(()) => {
let _ = writeln!(outcome.stderr, "wrote {}", file.path.display());
}
Err(error) => {
let _ = writeln!(
outcome.stderr,
"error: cannot write {}: {error}",
file.path.display()
);
outcome.exit = EXIT_IO;
break;
}
}
}
outcome
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing;
fn words(line: &str) -> Vec<String> {
line.split_whitespace().map(ToString::to_string).collect()
}
#[test]
fn no_arguments_print_the_command_list() {
let outcome = run_command_line(&[]);
assert_eq!(outcome.exit, EXIT_OK);
assert!(outcome.stdout.contains("info"), "{}", outcome.stdout);
assert_eq!(outcome.stderr, "");
}
#[test]
fn a_command_line_mistake_is_exit_1_with_a_hint() {
let outcome = run_command_line(&words("frobnicate x.asc"));
assert_eq!(outcome.exit, EXIT_USAGE);
assert_eq!(outcome.stdout, "");
assert!(
outcome.stderr.starts_with("error: unknown command"),
"{}",
outcome.stderr
);
assert!(
outcome.stderr.ends_with("try: indicatrix-cli --help\n"),
"{}",
outcome.stderr
);
}
#[test]
fn help_and_version_are_exit_0() {
for line in ["--help", "info --help", "help retarget", "--version"] {
let outcome = run_command_line(&words(line));
assert_eq!(outcome.exit, EXIT_OK, "{line}");
assert!(!outcome.stdout.is_empty(), "{line}");
}
}
#[test]
fn a_file_that_cannot_be_read_is_exit_3() {
let outcome = run_command_line(&words("info no-such-folder/no-such-file.indicatrix"));
assert_eq!(outcome.exit, EXIT_IO, "{}", outcome.stderr);
assert!(
outcome.stderr.starts_with("error: cannot read"),
"{}",
outcome.stderr
);
}
#[test]
fn run_writes_the_files_and_says_so() {
let input = testing::write_design("lib-run.indicatrix", &testing::template());
let target = testing::temp_path("lib-run-out.indicatrix");
let _ = std::fs::remove_file(&target);
let outcome = run(&[
"solve".to_string(),
input,
"--out".to_string(),
target.to_string_lossy().into_owned(),
]);
assert_eq!(outcome.exit, EXIT_OK, "{}", outcome.stderr);
assert!(target.is_file(), "the file is on disk");
assert!(outcome.stderr.contains("wrote "), "{}", outcome.stderr);
let text = std::fs::read_to_string(&target).expect("readable");
assert_ne!(text, "");
}
#[test]
fn run_ends_with_exit_3_when_a_file_cannot_be_written() {
let input = testing::write_design("lib-run-bad.indicatrix", &testing::template());
let target = testing::temp_path("no-such-folder").join("out.indicatrix");
let outcome = run(&[
"solve".to_string(),
input,
"--out".to_string(),
target.to_string_lossy().into_owned(),
]);
assert_eq!(outcome.exit, EXIT_IO, "{}", outcome.stderr);
assert!(
outcome.stderr.contains("error: cannot write"),
"{}",
outcome.stderr
);
}
#[test]
fn a_refused_command_leaves_no_file_behind() {
let target = testing::temp_path("lib-refused.indicatrix");
let _ = std::fs::remove_file(&target);
let outcome = run(&[
"solve".to_string(),
"no-such-file.indicatrix".to_string(),
"--out".to_string(),
target.to_string_lossy().into_owned(),
]);
assert_eq!(outcome.exit, EXIT_IO);
assert!(!target.exists());
}
}