use std::cell::OnceCell;
use std::env::consts::ARCH;
use std::fs::{self, File};
use std::io::{stdout, IsTerminal as _};
use std::path::{Path, PathBuf};
use std::process::exit;
use std::{env, io};
use anyhow::{Context, Result};
use clap::Parser;
use env_logger::{fmt::Target as LogTarget, Builder};
use regex::Regex;
use vmtest::{Config, Target, Ui, VMConfig, Vmtest};
const HELP_ENV_VARS: &str = r#"Environment variables:
VMTEST_NO_UI Set to disable UI [default: unset]
"#;
#[derive(Parser, Debug)]
#[clap(version, disable_colored_help=true, after_help=HELP_ENV_VARS)]
struct Args {
#[clap(short, long)]
config: Option<PathBuf>,
#[clap(short, long, default_value = ".*")]
filter: String,
#[clap(short, long, conflicts_with = "config")]
kernel: Option<PathBuf>,
#[clap(long, conflicts_with = "config")]
kargs: Option<String>,
#[clap(short, long, conflicts_with = "config", default_value = Target::default_rootfs().into_os_string())]
rootfs: PathBuf,
#[clap(short, long, default_value = ARCH, conflicts_with = "config")]
arch: String,
#[clap(short, long, conflicts_with = "config")]
qemu_command: Option<String>,
#[clap(conflicts_with = "config")]
command: Vec<String>,
}
#[derive(Default)]
struct DeferredLog {
file: OnceCell<File>,
}
impl DeferredLog {
fn file(&mut self) -> &File {
self.file.get_or_init(|| {
fs::OpenOptions::new()
.create(true)
.append(true)
.open(".vmtest.log")
.unwrap_or_else(|err| panic!("failed to create .vmtest.log: {err}"))
})
}
}
impl io::Write for DeferredLog {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file().flush()
}
}
fn init_logging() -> Result<()> {
let target = match stdout().is_terminal() {
false => LogTarget::Stderr,
true => LogTarget::Pipe(Box::new(DeferredLog::default())),
};
Builder::from_default_env()
.default_format()
.target(target)
.try_init()
.context("Failed to init env_logger")?;
Ok(())
}
fn config(args: &Args) -> Result<Vmtest> {
match &args.kernel {
Some(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()),
rootfs: args.rootfs.clone(),
arch: args.arch.clone(),
kernel_args: args.kargs.clone(),
qemu_command: args.qemu_command.clone(),
command: args.command.join(" "),
vm: VMConfig::default(),
}],
};
Vmtest::new(cwd, config)
}
None => {
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 filter = Regex::new(&args.filter).context("Failed to compile regex")?;
let mut config: Config = toml::from_str(&contents).context("Failed to parse config")?;
config.target = config
.target
.into_iter()
.filter(|t| filter.is_match(&t.name))
.collect::<Vec<_>>();
let base = config_path.parent().unwrap();
Vmtest::new(base, config)
}
}
}
fn show_cmd(args: &Args) -> bool {
args.kernel.is_some()
}
fn main() -> Result<()> {
let args = Args::parse();
init_logging().context("Failed to initialize logging")?;
let vmtest = config(&args)?;
let ui = Ui::new(vmtest);
let rc = ui.run(show_cmd(&args));
exit(rc);
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{Builder, TempDir};
fn test_config() -> Result<TempDir> {
let tmp_dir = Builder::new().tempdir()?;
let config_path = tmp_dir.path().join("vmtest.toml");
fs::write(
&config_path,
r#"
[[target]]
name = "test1"
image = "test1.img"
command = "echo test1"
[[target]]
name = "test2"
kernel = "test2.kernel"
command = "echo test2"
"#,
)
.unwrap();
Ok(tmp_dir)
}
#[test]
fn test_config_no_filter() {
let tmp_dir = test_config().expect("Failed to create config");
let config_path = tmp_dir.path().join("vmtest.toml");
let args = Args::parse_from([
"cliname",
"-c",
config_path.to_str().expect("Failed to create config path"),
]);
let vmtest = config(&args).expect("Failed to parse config");
assert_eq!(vmtest.targets().len(), 2);
}
#[test]
fn test_config_filter_match_all() {
let tmp_dir = test_config().expect("Failed to create config");
let config_path = tmp_dir.path().join("vmtest.toml");
let args = Args::parse_from([
"cliname",
"-c",
config_path.to_str().expect("Failed to create config path"),
"-f",
"test",
]);
let vmtest = config(&args).expect("Failed to parse config");
assert_eq!(vmtest.targets().len(), 2);
}
#[test]
fn test_config_filter_match_last() {
let tmp_dir = test_config().expect("Failed to create config");
let config_path = tmp_dir.path().join("vmtest.toml");
let args = Args::parse_from([
"cliname",
"-c",
config_path.to_str().expect("Failed to create config path"),
"-f",
"test2",
]);
let vmtest = config(&args).expect("Failed to parse config");
assert_eq!(vmtest.targets().len(), 1);
assert_eq!(vmtest.targets()[0].name, "test2");
}
#[test]
fn test_config_with_kernel_ignore_filter() {
let args = Args::parse_from(["cliname", "-k", "mykernel", "-f", "test2", "command to run"]);
let vmtest = config(&args).expect("Failed to parse config");
assert_eq!(vmtest.targets().len(), 1);
assert_eq!(vmtest.targets()[0].name, "mykernel");
}
}