From b87d55f960178971652df95257ddd1ea4f39c6c0 Mon Sep 17 00:00:00 2001 From: datdenkikniet Date: Sun, 16 Apr 2023 11:12:44 +0200 Subject: Move run into a subdirectory and split `iter` stuff into a module --- xtask/src/run.rs | 553 -------------------------------------------------- xtask/src/run/iter.rs | 48 +++++ xtask/src/run/mod.rs | 487 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 535 insertions(+), 553 deletions(-) delete mode 100644 xtask/src/run.rs create mode 100644 xtask/src/run/iter.rs create mode 100644 xtask/src/run/mod.rs (limited to 'xtask/src') diff --git a/xtask/src/run.rs b/xtask/src/run.rs deleted file mode 100644 index 49437d7..0000000 --- a/xtask/src/run.rs +++ /dev/null @@ -1,553 +0,0 @@ -use std::{ - fs::File, - io::Read, - path::PathBuf, - process::{Command, Stdio}, -}; - -use crate::{ - argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Globals, PackageOpt, TestMetadata}, - cargo_command::{BuildMode, CargoCommand}, - command_parser, RunResult, TestRunError, -}; -use log::{error, info, Level}; - -#[cfg(feature = "rayon")] -use rayon::prelude::*; - -use iters::*; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum OutputMode { - PipedAndCollected, - Inherited, -} - -impl From for Stdio { - fn from(value: OutputMode) -> Self { - match value { - OutputMode::PipedAndCollected => Stdio::piped(), - OutputMode::Inherited => Stdio::inherit(), - } - } -} - -#[derive(Debug)] -pub enum FinalRunResult<'c> { - Success(CargoCommand<'c>, RunResult), - Failed(CargoCommand<'c>, RunResult), - CommandError(CargoCommand<'c>, anyhow::Error), -} - -fn run_and_convert<'a>( - (global, command, overwrite): (&Globals, CargoCommand<'a>, bool), -) -> FinalRunResult<'a> { - // Run the command - let result = command_parser(global, &command, overwrite); - - let output = match result { - // If running the command succeeded without looking at any of the results, - // log the data and see if the actual execution was succesfull too. - Ok(result) => { - if result.exit_status.success() { - FinalRunResult::Success(command, result) - } else { - FinalRunResult::Failed(command, result) - } - } - // If it didn't and some IO error occured, just panic - Err(e) => FinalRunResult::CommandError(command, e), - }; - - log::trace!("Final result: {output:?}"); - - output -} - -pub trait CoalescingRunner<'c> { - /// Run all the commands in this iterator, and coalesce the results into - /// one error (if any individual commands failed) - fn run_and_coalesce(self) -> Vec>; -} - -#[cfg(not(feature = "rayon"))] -mod iters { - use super::*; - - pub fn examples_iter(examples: &[String]) -> impl Iterator { - examples.into_iter() - } - - impl<'g, 'c, I> CoalescingRunner<'c> for I - where - I: Iterator, bool)>, - { - fn run_and_coalesce(self) -> Vec> { - self.map(run_and_convert).collect() - } - } -} - -#[cfg(feature = "rayon")] -mod iters { - use super::*; - - pub fn examples_iter(examples: &[String]) -> impl ParallelIterator { - examples.into_par_iter() - } - - impl<'g, 'c, I> CoalescingRunner<'c> for I - where - I: ParallelIterator, bool)>, - { - fn run_and_coalesce(self) -> Vec> { - self.map(run_and_convert).collect() - } - } -} - -/// Cargo command to either build or check -pub fn cargo<'c>( - globals: &Globals, - operation: BuildOrCheck, - cargoarg: &'c Option<&'c str>, - package: &'c PackageOpt, - backend: Backends, -) -> Vec> { - let runner = package - .packages() - .flat_map(|package| { - let target = backend.to_target(); - let features = package.features(target, backend, globals.partial); - - #[cfg(feature = "rayon")] - { - features.into_par_iter().map(move |f| (package, target, f)) - } - - #[cfg(not(feature = "rayon"))] - { - features.into_iter().map(move |f| (package, target, f)) - } - }) - .map(move |(package, target, features)| { - let target = target.into(); - let command = match operation { - BuildOrCheck::Check => CargoCommand::Check { - cargoarg, - package: Some(package.name()), - target, - features, - mode: BuildMode::Release, - dir: None, - }, - BuildOrCheck::Build => CargoCommand::Build { - cargoarg, - package: Some(package.name()), - target, - features, - mode: BuildMode::Release, - dir: None, - }, - }; - - (globals, command, false) - }); - - runner.run_and_coalesce() -} - -/// Cargo command to build a usage example. -/// -/// The usage examples are in examples/ -pub fn cargo_usage_example( - globals: &Globals, - operation: BuildOrCheck, - usage_examples: Vec, -) -> Vec> { - examples_iter(&usage_examples) - .map(|example| { - let path = format!("examples/{example}"); - - let command = match operation { - BuildOrCheck::Check => CargoCommand::Check { - cargoarg: &None, - mode: BuildMode::Release, - dir: Some(path.into()), - package: None, - target: None, - features: None, - }, - BuildOrCheck::Build => CargoCommand::Build { - cargoarg: &None, - package: None, - target: None, - features: None, - mode: BuildMode::Release, - dir: Some(path.into()), - }, - }; - (globals, command, false) - }) - .run_and_coalesce() -} - -/// Cargo command to either build or check all examples -/// -/// The examples are in rtic/examples -pub fn cargo_example<'c>( - globals: &Globals, - operation: BuildOrCheck, - cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], -) -> Vec> { - let runner = examples_iter(examples).map(|example| { - let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); - - let command = match operation { - BuildOrCheck::Check => CargoCommand::ExampleCheck { - cargoarg, - example, - target: Some(backend.to_target()), - features, - mode: BuildMode::Release, - }, - BuildOrCheck::Build => CargoCommand::ExampleBuild { - cargoarg, - example, - target: Some(backend.to_target()), - features, - mode: BuildMode::Release, - dir: Some(PathBuf::from("./rtic")), - }, - }; - (globals, command, false) - }); - runner.run_and_coalesce() -} - -/// Run cargo clippy on selected package -pub fn cargo_clippy<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - package: &'c PackageOpt, - backend: Backends, -) -> Vec> { - let runner = package - .packages() - .flat_map(|package| { - let target = backend.to_target(); - let features = package.features(target, backend, globals.partial); - - #[cfg(feature = "rayon")] - { - features.into_par_iter().map(move |f| (package, target, f)) - } - - #[cfg(not(feature = "rayon"))] - { - features.into_iter().map(move |f| (package, target, f)) - } - }) - .map(move |(package, target, features)| { - let command = CargoCommand::Clippy { - cargoarg, - package: Some(package.name()), - target: target.into(), - features, - }; - - (globals, command, false) - }); - - runner.run_and_coalesce() -} - -/// Run cargo fmt on selected package -pub fn cargo_format<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - package: &'c PackageOpt, - check_only: bool, -) -> Vec> { - let runner = package.packages().map(|p| { - ( - globals, - CargoCommand::Format { - cargoarg, - package: Some(p.name()), - check_only, - }, - false, - ) - }); - runner.run_and_coalesce() -} - -/// Run cargo doc -pub fn cargo_doc<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - arguments: &'c Option, -) -> Vec> { - let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); - - let command = CargoCommand::Doc { - cargoarg, - features, - arguments: arguments.clone(), - }; - - vec![run_and_convert((globals, command, false))] -} - -/// Run cargo test on the selected package or all packages -/// -/// If no package is specified, loop through all packages -pub fn cargo_test<'c>( - globals: &Globals, - package: &'c PackageOpt, - backend: Backends, -) -> Vec> { - package - .packages() - .map(|p| (globals, TestMetadata::match_package(p, backend), false)) - .run_and_coalesce() -} - -/// Use mdbook to build the book -pub fn cargo_book<'c>( - globals: &Globals, - arguments: &'c Option, -) -> Vec> { - vec![run_and_convert(( - globals, - CargoCommand::Book { - arguments: arguments.clone(), - }, - false, - ))] -} - -/// Run examples -/// -/// Supports updating the expected output via the overwrite argument -pub fn qemu_run_examples<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], - overwrite: bool, -) -> Vec> { - let target = backend.to_target(); - let features = Some(target.and_features(backend.to_rtic_feature())); - - examples_iter(examples) - .flat_map(|example| { - let target = target.into(); - let cmd_build = CargoCommand::ExampleBuild { - cargoarg: &None, - example, - target, - features: features.clone(), - mode: BuildMode::Release, - dir: Some(PathBuf::from("./rtic")), - }; - - let cmd_qemu = CargoCommand::Qemu { - cargoarg, - example, - target, - features: features.clone(), - mode: BuildMode::Release, - dir: Some(PathBuf::from("./rtic")), - }; - - #[cfg(not(feature = "rayon"))] - { - [cmd_build, cmd_qemu].into_iter() - } - - #[cfg(feature = "rayon")] - { - [cmd_build, cmd_qemu].into_par_iter() - } - }) - .map(|cmd| (globals, cmd, overwrite)) - .run_and_coalesce() -} - -/// Check the binary sizes of examples -pub fn build_and_check_size<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], - arguments: &'c Option, -) -> Vec> { - let target = backend.to_target(); - let features = Some(target.and_features(backend.to_rtic_feature())); - - let runner = examples_iter(examples).map(|example| { - let target = target.into(); - - // Make sure the requested example(s) are built - let cmd = CargoCommand::ExampleBuild { - cargoarg: &Some("--quiet"), - example, - target, - features: features.clone(), - mode: BuildMode::Release, - dir: Some(PathBuf::from("./rtic")), - }; - if let Err(err) = command_parser(globals, &cmd, false) { - error!("{err}"); - } - - let cmd = CargoCommand::ExampleSize { - cargoarg, - example, - target, - features: features.clone(), - mode: BuildMode::Release, - arguments: arguments.clone(), - dir: Some(PathBuf::from("./rtic")), - }; - (globals, cmd, false) - }); - - runner.run_and_coalesce() -} - -pub fn run_command(command: &CargoCommand, stderr_mode: OutputMode) -> anyhow::Result { - log::info!("👟 {command}"); - - let mut process = Command::new(command.executable()); - - process - .args(command.args()) - .stdout(Stdio::piped()) - .stderr(stderr_mode); - - if let Some(dir) = command.chdir() { - process.current_dir(dir.canonicalize()?); - } - - let result = process.output()?; - - let exit_status = result.status; - let stderr = String::from_utf8(result.stderr).unwrap_or("Not displayable".into()); - let stdout = String::from_utf8(result.stdout).unwrap_or("Not displayable".into()); - - if command.print_stdout_intermediate() && exit_status.success() { - log::info!("\n{}", stdout); - } - - if exit_status.success() { - log::info!("✅ Success.") - } else { - log::error!("❌ Command failed. Run to completion for the summary."); - } - - Ok(RunResult { - exit_status, - stdout, - stderr, - }) -} - -/// Check if `run` was successful. -/// returns Ok in case the run went as expected, -/// Err otherwise -pub fn run_successful(run: &RunResult, expected_output_file: &str) -> Result<(), TestRunError> { - let mut file_handle = - File::open(expected_output_file).map_err(|_| TestRunError::FileError { - file: expected_output_file.to_owned(), - })?; - let mut expected_output = String::new(); - file_handle - .read_to_string(&mut expected_output) - .map_err(|_| TestRunError::FileError { - file: expected_output_file.to_owned(), - })?; - - if expected_output != run.stdout { - Err(TestRunError::FileCmpError { - expected: expected_output.clone(), - got: run.stdout.clone(), - }) - } else if !run.exit_status.success() { - Err(TestRunError::CommandError(run.clone())) - } else { - Ok(()) - } -} - -pub fn handle_results(globals: &Globals, results: Vec) -> Result<(), ()> { - let errors = results.iter().filter_map(|r| { - if let FinalRunResult::Failed(c, r) = r { - Some((c, &r.stdout, &r.stderr)) - } else { - None - } - }); - - let successes = results.iter().filter_map(|r| { - if let FinalRunResult::Success(c, r) = r { - Some((c, &r.stdout, &r.stderr)) - } else { - None - } - }); - - let command_errors = results.iter().filter_map(|r| { - if let FinalRunResult::CommandError(c, e) = r { - Some((c, e)) - } else { - None - } - }); - - let log_stdout_stderr = |level: Level| { - move |(cmd, stdout, stderr): (&CargoCommand, &String, &String)| { - let cmd = cmd.as_cmd_string(); - if !stdout.is_empty() && !stderr.is_empty() { - log::log!(level, "\n{cmd}\nStdout:\n{stdout}\nStderr:\n{stderr}"); - } else if !stdout.is_empty() { - log::log!(level, "\n{cmd}\nStdout:\n{}", stdout.trim_end()); - } else if !stderr.is_empty() { - log::log!(level, "\n{cmd}\nStderr:\n{}", stderr.trim_end()); - } - } - }; - - successes.for_each(|(cmd, stdout, stderr)| { - if globals.verbose > 0 { - info!("✅ Success: {cmd}\n {}", cmd.as_cmd_string()); - } else { - info!("✅ Success: {cmd}"); - } - - log_stdout_stderr(Level::Debug)((cmd, stdout, stderr)); - }); - - errors.clone().for_each(|(cmd, stdout, stderr)| { - error!("❌ Failed: {cmd}\n {}", cmd.as_cmd_string()); - log_stdout_stderr(Level::Error)((cmd, stdout, stderr)); - }); - - command_errors - .clone() - .for_each(|(cmd, error)| error!("❌ Failed: {cmd}\n {}\n{error}", cmd.as_cmd_string())); - - let ecount = errors.count() + command_errors.count(); - if ecount != 0 { - log::error!("{ecount} commands failed."); - Err(()) - } else { - info!("🚀🚀🚀 All tasks succeeded 🚀🚀🚀"); - Ok(()) - } -} diff --git a/xtask/src/run/iter.rs b/xtask/src/run/iter.rs new file mode 100644 index 0000000..d18ad49 --- /dev/null +++ b/xtask/src/run/iter.rs @@ -0,0 +1,48 @@ +use super::FinalRunResult; + +pub use iter::*; + +pub trait CoalescingRunner<'c> { + /// Run all the commands in this iterator, and coalesce the results into + /// one error (if any individual commands failed) + fn run_and_coalesce(self) -> Vec>; +} + +#[cfg(not(feature = "rayon"))] +mod iter { + use super::*; + use crate::{argument_parsing::Globals, cargo_command::*, run::run_and_convert}; + + pub fn into_iter(var: T) -> impl Iterator { + var.into_iter() + } + + impl<'g, 'c, I> CoalescingRunner<'c> for I + where + I: Iterator, bool)>, + { + fn run_and_coalesce(self) -> Vec> { + self.map(run_and_convert).collect() + } + } +} + +#[cfg(feature = "rayon")] +mod iter { + use super::*; + use crate::{argument_parsing::Globals, cargo_command::*, run::run_and_convert}; + use rayon::prelude::*; + + pub fn into_iter(var: T) -> impl ParallelIterator { + var.into_par_iter() + } + + impl<'g, 'c, I> CoalescingRunner<'c> for I + where + I: ParallelIterator, bool)>, + { + fn run_and_coalesce(self) -> Vec> { + self.map(run_and_convert).collect() + } + } +} diff --git a/xtask/src/run/mod.rs b/xtask/src/run/mod.rs new file mode 100644 index 0000000..daa6256 --- /dev/null +++ b/xtask/src/run/mod.rs @@ -0,0 +1,487 @@ +use std::{ + fs::File, + io::Read, + path::PathBuf, + process::{Command, Stdio}, +}; + +mod iter; +use iter::{into_iter, CoalescingRunner}; + +use crate::{ + argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Globals, PackageOpt, TestMetadata}, + cargo_command::{BuildMode, CargoCommand}, + command_parser, RunResult, TestRunError, +}; + +use log::{error, info, Level}; + +#[cfg(feature = "rayon")] +use rayon::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum OutputMode { + PipedAndCollected, + Inherited, +} + +impl From for Stdio { + fn from(value: OutputMode) -> Self { + match value { + OutputMode::PipedAndCollected => Stdio::piped(), + OutputMode::Inherited => Stdio::inherit(), + } + } +} + +#[derive(Debug)] +pub enum FinalRunResult<'c> { + Success(CargoCommand<'c>, RunResult), + Failed(CargoCommand<'c>, RunResult), + CommandError(CargoCommand<'c>, anyhow::Error), +} + +fn run_and_convert<'a>( + (global, command, overwrite): (&Globals, CargoCommand<'a>, bool), +) -> FinalRunResult<'a> { + // Run the command + let result = command_parser(global, &command, overwrite); + + let output = match result { + // If running the command succeeded without looking at any of the results, + // log the data and see if the actual execution was succesfull too. + Ok(result) => { + if result.exit_status.success() { + FinalRunResult::Success(command, result) + } else { + FinalRunResult::Failed(command, result) + } + } + // If it didn't and some IO error occured, just panic + Err(e) => FinalRunResult::CommandError(command, e), + }; + + log::trace!("Final result: {output:?}"); + + output +} + +/// Cargo command to either build or check +pub fn cargo<'c>( + globals: &Globals, + operation: BuildOrCheck, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, + backend: Backends, +) -> Vec> { + let runner = package + .packages() + .flat_map(|package| { + let target = backend.to_target(); + let features = package.features(target, backend, globals.partial); + into_iter(features).map(move |f| (package, target, f)) + }) + .map(move |(package, target, features)| { + let target = target.into(); + let command = match operation { + BuildOrCheck::Check => CargoCommand::Check { + cargoarg, + package: Some(package.name()), + target, + features, + mode: BuildMode::Release, + dir: None, + }, + BuildOrCheck::Build => CargoCommand::Build { + cargoarg, + package: Some(package.name()), + target, + features, + mode: BuildMode::Release, + dir: None, + }, + }; + + (globals, command, false) + }); + + runner.run_and_coalesce() +} + +/// Cargo command to build a usage example. +/// +/// The usage examples are in examples/ +pub fn cargo_usage_example( + globals: &Globals, + operation: BuildOrCheck, + usage_examples: Vec, +) -> Vec> { + into_iter(&usage_examples) + .map(|example| { + let path = format!("examples/{example}"); + + let command = match operation { + BuildOrCheck::Check => CargoCommand::Check { + cargoarg: &None, + mode: BuildMode::Release, + dir: Some(path.into()), + package: None, + target: None, + features: None, + }, + BuildOrCheck::Build => CargoCommand::Build { + cargoarg: &None, + package: None, + target: None, + features: None, + mode: BuildMode::Release, + dir: Some(path.into()), + }, + }; + (globals, command, false) + }) + .run_and_coalesce() +} + +/// Cargo command to either build or check all examples +/// +/// The examples are in rtic/examples +pub fn cargo_example<'c>( + globals: &Globals, + operation: BuildOrCheck, + cargoarg: &'c Option<&'c str>, + backend: Backends, + examples: &'c [String], +) -> Vec> { + let runner = into_iter(examples).map(|example| { + let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); + + let command = match operation { + BuildOrCheck::Check => CargoCommand::ExampleCheck { + cargoarg, + example, + target: Some(backend.to_target()), + features, + mode: BuildMode::Release, + }, + BuildOrCheck::Build => CargoCommand::ExampleBuild { + cargoarg, + example, + target: Some(backend.to_target()), + features, + mode: BuildMode::Release, + dir: Some(PathBuf::from("./rtic")), + }, + }; + (globals, command, false) + }); + runner.run_and_coalesce() +} + +/// Run cargo clippy on selected package +pub fn cargo_clippy<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, + backend: Backends, +) -> Vec> { + let runner = package + .packages() + .flat_map(|package| { + let target = backend.to_target(); + let features = package.features(target, backend, globals.partial); + into_iter(features).map(move |f| (package, target, f)) + }) + .map(move |(package, target, features)| { + let command = CargoCommand::Clippy { + cargoarg, + package: Some(package.name()), + target: target.into(), + features, + }; + + (globals, command, false) + }); + + runner.run_and_coalesce() +} + +/// Run cargo fmt on selected package +pub fn cargo_format<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, + check_only: bool, +) -> Vec> { + let runner = package.packages().map(|p| { + ( + globals, + CargoCommand::Format { + cargoarg, + package: Some(p.name()), + check_only, + }, + false, + ) + }); + runner.run_and_coalesce() +} + +/// Run cargo doc +pub fn cargo_doc<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + backend: Backends, + arguments: &'c Option, +) -> Vec> { + let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); + + let command = CargoCommand::Doc { + cargoarg, + features, + arguments: arguments.clone(), + }; + + vec![run_and_convert((globals, command, false))] +} + +/// Run cargo test on the selected package or all packages +/// +/// If no package is specified, loop through all packages +pub fn cargo_test<'c>( + globals: &Globals, + package: &'c PackageOpt, + backend: Backends, +) -> Vec> { + package + .packages() + .map(|p| (globals, TestMetadata::match_package(p, backend), false)) + .run_and_coalesce() +} + +/// Use mdbook to build the book +pub fn cargo_book<'c>( + globals: &Globals, + arguments: &'c Option, +) -> Vec> { + vec![run_and_convert(( + globals, + CargoCommand::Book { + arguments: arguments.clone(), + }, + false, + ))] +} + +/// Run examples +/// +/// Supports updating the expected output via the overwrite argument +pub fn qemu_run_examples<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + backend: Backends, + examples: &'c [String], + overwrite: bool, +) -> Vec> { + let target = backend.to_target(); + let features = Some(target.and_features(backend.to_rtic_feature())); + + into_iter(examples) + .flat_map(|example| { + let target = target.into(); + let cmd_build = CargoCommand::ExampleBuild { + cargoarg: &None, + example, + target, + features: features.clone(), + mode: BuildMode::Release, + dir: Some(PathBuf::from("./rtic")), + }; + + let cmd_qemu = CargoCommand::Qemu { + cargoarg, + example, + target, + features: features.clone(), + mode: BuildMode::Release, + dir: Some(PathBuf::from("./rtic")), + }; + + into_iter([cmd_build, cmd_qemu]) + }) + .map(|cmd| (globals, cmd, overwrite)) + .run_and_coalesce() +} + +/// Check the binary sizes of examples +pub fn build_and_check_size<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + backend: Backends, + examples: &'c [String], + arguments: &'c Option, +) -> Vec> { + let target = backend.to_target(); + let features = Some(target.and_features(backend.to_rtic_feature())); + + let runner = into_iter(examples).map(|example| { + let target = target.into(); + + // Make sure the requested example(s) are built + let cmd = CargoCommand::ExampleBuild { + cargoarg: &Some("--quiet"), + example, + target, + features: features.clone(), + mode: BuildMode::Release, + dir: Some(PathBuf::from("./rtic")), + }; + if let Err(err) = command_parser(globals, &cmd, false) { + error!("{err}"); + } + + let cmd = CargoCommand::ExampleSize { + cargoarg, + example, + target, + features: features.clone(), + mode: BuildMode::Release, + arguments: arguments.clone(), + dir: Some(PathBuf::from("./rtic")), + }; + (globals, cmd, false) + }); + + runner.run_and_coalesce() +} + +pub fn run_command(command: &CargoCommand, stderr_mode: OutputMode) -> anyhow::Result { + log::info!("👟 {command}"); + + let mut process = Command::new(command.executable()); + + process + .args(command.args()) + .stdout(Stdio::piped()) + .stderr(stderr_mode); + + if let Some(dir) = command.chdir() { + process.current_dir(dir.canonicalize()?); + } + + let result = process.output()?; + + let exit_status = result.status; + let stderr = String::from_utf8(result.stderr).unwrap_or("Not displayable".into()); + let stdout = String::from_utf8(result.stdout).unwrap_or("Not displayable".into()); + + if command.print_stdout_intermediate() && exit_status.success() { + log::info!("\n{}", stdout); + } + + if exit_status.success() { + log::info!("✅ Success.") + } else { + log::error!("❌ Command failed. Run to completion for the summary."); + } + + Ok(RunResult { + exit_status, + stdout, + stderr, + }) +} + +/// Check if `run` was successful. +/// returns Ok in case the run went as expected, +/// Err otherwise +pub fn run_successful(run: &RunResult, expected_output_file: &str) -> Result<(), TestRunError> { + let mut file_handle = + File::open(expected_output_file).map_err(|_| TestRunError::FileError { + file: expected_output_file.to_owned(), + })?; + let mut expected_output = String::new(); + file_handle + .read_to_string(&mut expected_output) + .map_err(|_| TestRunError::FileError { + file: expected_output_file.to_owned(), + })?; + + if expected_output != run.stdout { + Err(TestRunError::FileCmpError { + expected: expected_output.clone(), + got: run.stdout.clone(), + }) + } else if !run.exit_status.success() { + Err(TestRunError::CommandError(run.clone())) + } else { + Ok(()) + } +} + +pub fn handle_results(globals: &Globals, results: Vec) -> Result<(), ()> { + let errors = results.iter().filter_map(|r| { + if let FinalRunResult::Failed(c, r) = r { + Some((c, &r.stdout, &r.stderr)) + } else { + None + } + }); + + let successes = results.iter().filter_map(|r| { + if let FinalRunResult::Success(c, r) = r { + Some((c, &r.stdout, &r.stderr)) + } else { + None + } + }); + + let command_errors = results.iter().filter_map(|r| { + if let FinalRunResult::CommandError(c, e) = r { + Some((c, e)) + } else { + None + } + }); + + let log_stdout_stderr = |level: Level| { + move |(cmd, stdout, stderr): (&CargoCommand, &String, &String)| { + let cmd = cmd.as_cmd_string(); + if !stdout.is_empty() && !stderr.is_empty() { + log::log!(level, "\n{cmd}\nStdout:\n{stdout}\nStderr:\n{stderr}"); + } else if !stdout.is_empty() { + log::log!(level, "\n{cmd}\nStdout:\n{}", stdout.trim_end()); + } else if !stderr.is_empty() { + log::log!(level, "\n{cmd}\nStderr:\n{}", stderr.trim_end()); + } + } + }; + + successes.for_each(|(cmd, stdout, stderr)| { + if globals.verbose > 0 { + info!("✅ Success: {cmd}\n {}", cmd.as_cmd_string()); + } else { + info!("✅ Success: {cmd}"); + } + + log_stdout_stderr(Level::Debug)((cmd, stdout, stderr)); + }); + + errors.clone().for_each(|(cmd, stdout, stderr)| { + error!("❌ Failed: {cmd}\n {}", cmd.as_cmd_string()); + log_stdout_stderr(Level::Error)((cmd, stdout, stderr)); + }); + + command_errors + .clone() + .for_each(|(cmd, error)| error!("❌ Failed: {cmd}\n {}\n{error}", cmd.as_cmd_string())); + + let ecount = errors.count() + command_errors.count(); + if ecount != 0 { + log::error!("{ecount} commands failed."); + Err(()) + } else { + info!("🚀🚀🚀 All tasks succeeded 🚀🚀🚀"); + Ok(()) + } +} -- cgit v1.2.3