diff --git a/src/main.rs b/src/main.rs index 6e26dcb..fb04ae8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ +use std::env; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::exit; use anyhow::{Context, Result}; use clap::Parser; use regex::Regex; -use ::vmtest::{Ui, Vmtest}; +use ::vmtest::{Config, Target, Ui, Vmtest}; #[derive(Parser, Debug)] #[clap(version)] struct Args { /// Path to config file - #[clap(short, long, default_value = "vmtest.toml")] - config: PathBuf, + #[clap(short, long)] + config: Option, /// Filter by regex which targets to run /// /// This option takes a regular expression. If a target matches this regular @@ -22,19 +23,54 @@ struct Args { /// Supported regex syntax: https://docs.rs/regex/latest/regex/#syntax. #[clap(short, long, default_value = ".*")] filter: String, + #[clap(short, long, conflicts_with = "config")] + kernel: Option, + #[clap(conflicts_with = "config")] + command: Vec, +} + +/// Configure a `Vmtest` instance from command line arguments. +fn config(args: &Args) -> Result { + if let Some(kernel) = &args.kernel { + let cwd = env::current_dir().context("Failed to get current directory")?; + let config = Config { + target: vec![Target { + name: kernel.file_name().unwrap().to_string_lossy().to_string(), + image: None, + uefi: false, + kernel: Some(kernel.clone()), + kernel_args: None, + command: args.command.join(" "), + }], + }; + + Vmtest::new(cwd, config) + } else { + let default = Path::new("vmtest.toml").to_owned(); + let config_path = args.config.as_ref().unwrap_or(&default); + let contents = fs::read_to_string(config_path).context("Failed to read config file")?; + let config = toml::from_str(&contents).context("Failed to parse config")?; + let base = config_path.parent().unwrap(); + + Vmtest::new(base, config) + } +} + +/// Whether or not to collapse command output in UI. +/// +/// This is useful for one-liner invocations. +fn show_cmd(args: &Args) -> bool { + args.config.is_none() } fn main() -> Result<()> { let args = Args::parse(); env_logger::init(); - let contents = fs::read_to_string(&args.config).context("Failed to read config file")?; - let config = toml::from_str(&contents).context("Failed to parse config")?; - let base = args.config.parent().unwrap(); - let vmtest = Vmtest::new(base, config)?; + let vmtest = config(&args)?; let filter = Regex::new(&args.filter).context("Failed to compile regex")?; let ui = Ui::new(vmtest); - let failed = ui.run(&filter); + let failed = ui.run(&filter, show_cmd(&args)); let rc = i32::from(failed != 0); exit(rc); diff --git a/src/ui.rs b/src/ui.rs index 6f444f6..ab0c09c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -159,7 +159,7 @@ impl Ui { /// UI for a single target. Must be run on its own thread. /// /// Returns if the target was successful or not> - fn target_ui(updates: Receiver, target: String) -> bool { + fn target_ui(updates: Receiver, target: String, show_cmd: bool) -> bool { let term = Term::stdout(); let mut stage = Stage::new(term.clone(), &heading(&target, 1), None); let mut stages = 0; @@ -201,21 +201,27 @@ impl Ui { stages += 1; } Output::Command(s) => stage.print_line(s, None), - Output::CommandEnd(r) => match r { - Ok(retval) => { - if *retval != 0 { - error_out_stage( - &mut stage, - &anyhow!("Command failed with exit code: {}", retval), - ); + Output::CommandEnd(r) => { + if show_cmd { + stage.expand(true); + } + + match r { + Ok(retval) => { + if *retval != 0 { + error_out_stage( + &mut stage, + &anyhow!("Command failed with exit code: {}", retval), + ); + errors += 1; + } + } + Err(e) => { + error_out_stage(&mut stage, e); errors += 1; } - } - Err(e) => { - error_out_stage(&mut stage, e); - errors += 1; - } - }, + }; + } } } @@ -223,10 +229,10 @@ impl Ui { drop(stage); // Only clear target stages if target was successful - if errors == 0 { + if errors == 0 && !show_cmd { clear_last_lines(&term, stages); term.write_line("PASS").expect("Failed to write terminal"); - } else { + } else if !show_cmd { term.write_line("FAILED").expect("Failed to write terminal"); } @@ -235,10 +241,13 @@ impl Ui { /// Run all the targets in the provided `vmtest` /// + /// `filter` specifies the regex to filter targets by. + /// `show_cmd` specifies if the command output should always be shown. + /// /// Note this function is "infallible" b/c on error it will display /// the appropriate error message to screen. Rather, it returns how /// many targets failed. - pub fn run(self, filter: &Regex) -> usize { + pub fn run(self, filter: &Regex, show_cmd: bool) -> usize { let mut failed = 0; for (idx, target) in self .vmtest @@ -251,7 +260,7 @@ impl Ui { // Start UI on its own thread b/c `Vmtest::run_one()` will block let name = target.name.clone(); - let ui = thread::spawn(move || Self::target_ui(receiver, name)); + let ui = thread::spawn(move || Self::target_ui(receiver, name, show_cmd)); // Run a target self.vmtest.run_one(idx, sender); diff --git a/tests/test.rs b/tests/test.rs index c38b7f9..4f87909 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -86,7 +86,7 @@ fn assert_no_error(recv: Receiver) { fn test_run() { let vmtest = vmtest("vmtest.toml.allgood"); let ui = Ui::new(vmtest); - let failed = ui.run(&*FILTER_ALL); + let failed = ui.run(&*FILTER_ALL, false); assert_eq!(failed, 0); }