use std::{ fmt::Write as _, io::BufReader, path::PathBuf, process::{Command, Stdio}, }; use anyhow::{Context as _, Result}; use cargo_metadata::{Artifact, CompilerMessage, Message, Target}; use clap::Parser; #[derive(Debug, Parser)] pub struct Options { /// Build and run the release target #[clap(long)] pub release: bool, /// The command used to wrap your application #[clap(short, long, default_value = "sudo -E")] pub runner: String, /// Arguments to pass to your application #[clap(name = "args", last = true)] pub run_args: Vec, } /// Build the project fn build(release: bool) -> Result> { let mut cmd = Command::new("cargo"); cmd.args([ "build", "--tests", "--message-format=json", "--package=integration-test", ]); if release { cmd.arg("--release"); } let mut cmd = cmd .stdout(Stdio::piped()) .spawn() .with_context(|| format!("failed to spawn {cmd:?}"))?; let reader = BufReader::new(cmd.stdout.take().unwrap()); let mut executables = Vec::new(); let mut compiler_messages = String::new(); for message in Message::parse_stream(reader) { #[allow(clippy::collapsible_match)] match message.context("valid JSON")? { Message::CompilerArtifact(Artifact { executable, target: Target { name, .. }, .. }) => { if let Some(executable) = executable { executables.push((name, executable.into())); } } Message::CompilerMessage(CompilerMessage { message, .. }) => { writeln!(&mut compiler_messages, "{message}").context("String write failed")? } _ => {} } } let status = cmd .wait() .with_context(|| format!("failed to wait for {cmd:?}"))?; match status.code() { Some(code) => match code { 0 => Ok(executables), code => Err(anyhow::anyhow!( "{cmd:?} exited with status code {code}:\n{compiler_messages}" )), }, None => Err(anyhow::anyhow!("{cmd:?} terminated by signal")), } } /// Build and run the project pub fn run(opts: Options) -> Result<()> { let Options { release, runner, run_args, } = opts; let binaries = build(release).context("error while building userspace application")?; let mut args = runner.trim().split_terminator(' '); let runner = args.next().ok_or(anyhow::anyhow!("no first argument"))?; let args = args.collect::>(); let mut failures = String::new(); for (name, binary) in binaries { let mut cmd = Command::new(runner); let cmd = cmd .args(args.iter()) .arg(binary) .args(run_args.iter()) .arg("--test-threads=1"); println!("{} running {cmd:?}", name); let status = cmd .status() .with_context(|| format!("failed to run {cmd:?}"))?; match status.code() { Some(code) => match code { 0 => {} code => writeln!(&mut failures, "{} exited with status code {code}", name) .context("String write failed")?, }, None => writeln!(&mut failures, "{} terminated by signal", name) .context("String write failed")?, } } if failures.is_empty() { Ok(()) } else { Err(anyhow::anyhow!("failures:\n{}", failures)) } }