diff --git a/Cargo.toml b/Cargo.toml index 50fbc61..6481242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,4 +50,5 @@ windows-sys = {version = "0.52.0", features = ["Win32_Foundation", "Win32_System [dev-dependencies] env_logger = "0.11.3" rand = "0.10.0" +test-case = "3.3.1" tiny_http = "0.12.0" diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index c411e37..f55e99c 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -16,6 +16,7 @@ use log::{error, info}; use process_lines_actions::InnerState; use std::env::consts::EXE_SUFFIX; use std::ffi::{OsStr, OsString}; +use std::mem; use std::path::{Path, PathBuf}; use std::process::{ExitStatus, Stdio}; use std::time::{Duration, Instant}; @@ -557,7 +558,8 @@ impl From for ProcessOutput { } /// collected statistics about the process execution. -#[derive(Default)] +#[derive(Debug, Default, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ProcessStatistics { /// peak memory usage in bytes. /// This is populated for sandboxed commands on systems @@ -565,6 +567,29 @@ pub struct ProcessStatistics { pub memory_peak: Option, } +impl ProcessStatistics { + /// Merge two `ProcessStatistics` into one, following a fixed set of aggregation rules: + /// + /// - `memory_peak`: the maximum of the two values is kept, since a merged peak + /// should reflect the highest peak observed across all runs. If only one side + /// has a value and the other is `None`, that value is used as-is. + pub fn merge(self, other: Self) -> Self { + Self { + memory_peak: match (self.memory_peak, other.memory_peak) { + (Some(a), Some(b)) => Some(a.max(b)), + (a, b) => a.or(b), + }, + } + } + + /// Merge another `ProcessStatistics` into `self` in place. + /// + /// See [`merge`](Self::merge) for the aggregation rules. + pub fn merge_mut(&mut self, other: Self) { + *self = mem::take(self).merge(other); + } +} + /// Output of a [`Command`](struct.Command.html) when it was executed with the /// [`run_capture`](struct.Command.html#method.run_capture) method. pub struct ProcessOutput { @@ -732,3 +757,43 @@ fn exe_suffix(file: &OsStr) -> OsString { path.push(EXE_SUFFIX); path } + +#[cfg(test)] +mod tests { + use super::ProcessStatistics; + use test_case::test_case; + + const fn stats(peak: Option) -> ProcessStatistics { + ProcessStatistics { memory_peak: peak } + } + + #[test_case(stats(None), stats(None), stats(None))] + #[test_case(stats(Some(100)), stats(None), stats(Some(100)))] + #[test_case(stats(None), stats(Some(100)), stats(Some(100)))] + #[test_case(stats(Some(300)), stats(Some(100)), stats(Some(300)))] + #[test_case(stats(Some(100)), stats(Some(300)), stats(Some(300)))] + #[test_case(stats(Some(42)), stats(Some(42)), stats(Some(42)))] + fn test_merge(lhs: ProcessStatistics, rhs: ProcessStatistics, expected: ProcessStatistics) { + { + let lhs = lhs.clone(); + let rhs = rhs.clone(); + assert_eq!(lhs.merge(rhs), expected); + } + + { + let mut lhs = lhs.clone(); + lhs.merge_mut(rhs); + assert_eq!(lhs, expected); + } + } + + #[test] + fn merge_mut_accumulate_over_multiple() { + let mut s = stats(None); + s.merge_mut(stats(Some(50))); + s.merge_mut(stats(Some(200))); + s.merge_mut(stats(None)); + s.merge_mut(stats(Some(150))); + assert_eq!(s.memory_peak, Some(200)); + } +}