Skip to main content

rskit_process/
result.rs

1//! Process execution result.
2
3use std::time::Duration;
4
5/// Result of a completed subprocess execution.
6#[derive(Debug, Clone)]
7#[non_exhaustive]
8pub struct ProcessResult {
9    /// Process exit code. None if the process was killed.
10    pub exit_code: Option<i32>,
11    /// Captured standard output as a string.
12    pub stdout: String,
13    /// Captured standard output as raw bytes before lossy UTF-8 conversion.
14    pub stdout_bytes: Vec<u8>,
15    /// Captured standard error as a string.
16    pub stderr: String,
17    /// Captured standard error as raw bytes before lossy UTF-8 conversion.
18    pub stderr_bytes: Vec<u8>,
19    /// Whether stdout exceeded the configured capture limit.
20    pub stdout_truncated: bool,
21    /// Whether stderr exceeded the configured capture limit.
22    pub stderr_truncated: bool,
23    /// Total duration the process ran.
24    pub duration: Duration,
25    /// Whether the process was killed due to timeout.
26    pub timed_out: bool,
27    /// Whether the process was cancelled by the caller.
28    pub cancelled: bool,
29}
30
31impl ProcessResult {
32    /// Build a process result from captured output bytes.
33    #[must_use]
34    #[allow(
35        clippy::too_many_arguments,
36        reason = "distinct completion fields of a public result; grouping them would introduce a new \
37                  public parameter type solely to satisfy the lint"
38    )]
39    pub fn completed(
40        exit_code: Option<i32>,
41        stdout_bytes: Vec<u8>,
42        stderr_bytes: Vec<u8>,
43        stdout_truncated: bool,
44        stderr_truncated: bool,
45        duration: Duration,
46        timed_out: bool,
47        cancelled: bool,
48    ) -> Self {
49        Self {
50            exit_code,
51            stdout: String::from_utf8_lossy(&stdout_bytes).into_owned(),
52            stdout_bytes,
53            stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(),
54            stderr_bytes,
55            stdout_truncated,
56            stderr_truncated,
57            duration,
58            timed_out,
59            cancelled,
60        }
61    }
62
63    /// Check if the process exited successfully (exit code 0).
64    ///
65    /// # Example
66    ///
67    /// ```
68    /// use rskit_process::ProcessResult;
69    /// use std::time::Duration;
70    ///
71    /// let result = ProcessResult::completed(
72    ///     Some(0),
73    ///     b"output".to_vec(),
74    ///     Vec::new(),
75    ///     false,
76    ///     false,
77    ///     Duration::from_secs(1),
78    ///     false,
79    ///     false,
80    /// );
81    ///
82    /// assert!(result.success());
83    /// ```
84    pub fn success(&self) -> bool {
85        self.exit_code == Some(0)
86    }
87
88    /// Verify the process exited successfully, returning an error if not.
89    ///
90    /// # Example
91    ///
92    /// ```no_run
93    /// use rskit_process::{ProcessConfig, ProcessSpec, run_with_cancel};
94    /// use tokio_util::sync::CancellationToken;
95    ///
96    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
97    /// let spec = ProcessSpec::new("echo").arg("hello");
98    /// let result = run_with_cancel(&spec, &ProcessConfig::default(), CancellationToken::new()).await?;
99    /// result.check()?;
100    /// # Ok(())
101    /// # }
102    /// ```
103    pub fn check(&self) -> crate::AppResult<&Self> {
104        if self.cancelled {
105            return Err(
106                crate::AppError::new(crate::ErrorCode::Cancelled, "process cancelled")
107                    .with_detail("cancelled", true),
108            );
109        }
110        if self.timed_out {
111            return Err(
112                crate::AppError::new(crate::ErrorCode::Timeout, "process timed out")
113                    .with_detail("timed_out", true),
114            );
115        }
116
117        match self.exit_code {
118            Some(0) => Ok(self),
119            Some(code) => Err(crate::AppError::new(
120                crate::ErrorCode::Internal,
121                format!("process exited with code {}", code),
122            )
123            .with_detail("exit_code", code)),
124            None => Err(
125                crate::AppError::new(crate::ErrorCode::Internal, "process was killed")
126                    .with_detail("killed", true),
127            ),
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::ErrorCode;
136
137    fn result(exit_code: Option<i32>, timed_out: bool, cancelled: bool) -> ProcessResult {
138        ProcessResult::completed(
139            exit_code,
140            b"out".to_vec(),
141            b"err".to_vec(),
142            false,
143            true,
144            Duration::from_millis(5),
145            timed_out,
146            cancelled,
147        )
148    }
149
150    #[test]
151    fn completed_result_preserves_bytes_strings_flags_and_duration() {
152        let result = result(Some(0), false, false);
153
154        assert!(result.success());
155        assert_eq!(result.stdout, "out");
156        assert_eq!(result.stdout_bytes, b"out");
157        assert_eq!(result.stderr, "err");
158        assert_eq!(result.stderr_bytes, b"err");
159        assert!(!result.stdout_truncated);
160        assert!(result.stderr_truncated);
161        assert_eq!(result.duration, Duration::from_millis(5));
162        assert!(std::ptr::eq(result.check().unwrap(), &result));
163    }
164
165    #[test]
166    fn check_reports_cancelled_timeout_failed_and_killed_states() {
167        assert_eq!(
168            result(Some(0), false, true).check().unwrap_err().code(),
169            ErrorCode::Cancelled
170        );
171        assert_eq!(
172            result(Some(0), true, false).check().unwrap_err().code(),
173            ErrorCode::Timeout
174        );
175        assert_eq!(
176            result(Some(2), false, false).check().unwrap_err().code(),
177            ErrorCode::Internal
178        );
179        assert_eq!(
180            result(None, false, false).check().unwrap_err().code(),
181            ErrorCode::Internal
182        );
183        assert!(!result(Some(2), false, false).success());
184    }
185}