Skip to main content

rusty_pee/
aggregate.rs

1//! Exit-code aggregation across N child processes.
2//!
3//! Two strategies per FR-007 / FR-008:
4//! - **Default mode** ([`default_max`]): `max(child_codes)` — intuitive
5//!   "worst child wins" semantic.
6//! - **Strict mode** ([`strict_or`]): bitwise OR over `WEXITSTATUS` — byte-equal
7//!   moreutils `close_pipes()`.
8//!
9//! Signal-killed children contribute the code 1 (matches moreutils
10//! `WIFEXITED` fallthrough — non-`WIFEXITED` `wait` result → ret |= 1).
11
12/// Aggregate child exit codes using `max()` (Default mode, FR-007).
13///
14/// Empty input returns 0 (matches `--no-commands` Default-mode behavior).
15#[must_use]
16pub fn default_max(codes: &[i32]) -> i32 {
17    codes.iter().copied().max().unwrap_or(0)
18}
19
20/// Aggregate child exit codes using bitwise OR (Strict mode, FR-008).
21///
22/// Matches moreutils' `ret |= WEXITSTATUS(r)` aggregation byte-for-byte for
23/// the documented matrix `{(0,0)=0, (0,1)=1, (1,2)=3, (2,1)=3, (255,1)=255}`.
24/// Empty input returns 0.
25#[must_use]
26pub fn strict_or(codes: &[i32]) -> i32 {
27    codes.iter().copied().fold(0i32, |acc, c| acc | c)
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn default_max_empty_is_zero() {
36        assert_eq!(default_max(&[]), 0);
37    }
38
39    #[test]
40    fn default_max_single_passes_through() {
41        assert_eq!(default_max(&[0]), 0);
42        assert_eq!(default_max(&[1]), 1);
43        assert_eq!(default_max(&[42]), 42);
44        assert_eq!(default_max(&[255]), 255);
45    }
46
47    #[test]
48    fn default_max_matrix() {
49        // FR-007 / SC-002 matrix
50        assert_eq!(default_max(&[0, 0]), 0);
51        assert_eq!(default_max(&[0, 1]), 1);
52        assert_eq!(default_max(&[1, 0]), 1);
53        assert_eq!(default_max(&[2, 1]), 2);
54        assert_eq!(default_max(&[1, 2]), 2);
55        assert_eq!(default_max(&[255, 1]), 255);
56    }
57
58    #[test]
59    fn strict_or_empty_is_zero() {
60        assert_eq!(strict_or(&[]), 0);
61    }
62
63    #[test]
64    fn strict_or_single_passes_through() {
65        assert_eq!(strict_or(&[0]), 0);
66        assert_eq!(strict_or(&[1]), 1);
67        assert_eq!(strict_or(&[42]), 42);
68        assert_eq!(strict_or(&[255]), 255);
69    }
70
71    #[test]
72    fn strict_or_matrix_matches_moreutils() {
73        // FR-008 / SC-003 matrix — bitwise OR, byte-equal moreutils 0.69
74        assert_eq!(strict_or(&[0, 0]), 0);
75        assert_eq!(strict_or(&[0, 1]), 1);
76        assert_eq!(strict_or(&[1, 0]), 1);
77        // Key divergence vs Default: (1,2) → OR=3, max=2
78        assert_eq!(strict_or(&[2, 1]), 3);
79        assert_eq!(strict_or(&[1, 2]), 3);
80        assert_eq!(strict_or(&[255, 1]), 255);
81    }
82
83    #[test]
84    fn modes_diverge_on_distinct_nonzero_codes() {
85        // The defining behavioral divergence between modes (FR-007 vs FR-008).
86        let codes = &[1, 2];
87        assert_eq!(default_max(codes), 2, "Default mode: max");
88        assert_eq!(strict_or(codes), 3, "Strict mode: bitwise OR");
89    }
90}