qubit_command/command_output.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10#[cfg(unix)]
11use std::os::unix::process::ExitStatusExt;
12use std::{
13 borrow::Cow,
14 process::ExitStatus,
15 str,
16 time::Duration,
17};
18
19/// Captured output and status information from a finished command.
20///
21/// `CommandOutput` stores retained raw stdout and stderr bytes. When the runner
22/// is configured with per-stream capture limits, the retained bytes may be a
23/// prefix of the full output; use [`Self::stdout_truncated`] and
24/// [`Self::stderr_truncated`] to detect that case. [`Self::stdout`] and
25/// [`Self::stderr`] return raw bytes exactly as retained. Use
26/// [`Self::stdout_text`] and [`Self::stderr_text`] for strict UTF-8 text, or
27/// [`Self::stdout_lossy_text`] and [`Self::stderr_lossy_text`] to replace
28/// invalid byte sequences with the Unicode replacement character.
29///
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CommandOutput {
32 /// Exit status reported by the process.
33 status: ExitStatus,
34 /// Captured standard output bytes.
35 stdout: Vec<u8>,
36 /// Captured standard error bytes.
37 stderr: Vec<u8>,
38 /// Whether stdout was truncated by the configured capture limit.
39 stdout_truncated: bool,
40 /// Whether stderr was truncated by the configured capture limit.
41 stderr_truncated: bool,
42 /// Duration from process spawn to observed termination.
43 elapsed: Duration,
44}
45
46impl CommandOutput {
47 /// Creates command output from captured process data.
48 ///
49 /// # Parameters
50 ///
51 /// * `status` - Process exit status.
52 /// * `stdout` - Captured standard output bytes.
53 /// * `stderr` - Captured standard error bytes.
54 /// * `stdout_truncated` - Whether stdout exceeded the capture limit.
55 /// * `stderr_truncated` - Whether stderr exceeded the capture limit.
56 /// * `elapsed` - Observed process duration.
57 /// # Returns
58 ///
59 /// A command output value containing the supplied data.
60 #[inline]
61 pub(crate) fn new(
62 status: ExitStatus,
63 stdout: Vec<u8>,
64 stderr: Vec<u8>,
65 stdout_truncated: bool,
66 stderr_truncated: bool,
67 elapsed: Duration,
68 ) -> Self {
69 Self {
70 status,
71 stdout,
72 stderr,
73 stdout_truncated,
74 stderr_truncated,
75 elapsed,
76 }
77 }
78
79 /// Returns the command exit code.
80 ///
81 /// # Returns
82 ///
83 /// `Some(code)` when the platform reports a numeric process exit code, or
84 /// `None` when the process ended in a way that does not map to a numeric
85 /// code.
86 #[inline]
87 pub fn exit_code(&self) -> Option<i32> {
88 self.status.code()
89 }
90
91 /// Returns the full process exit status.
92 ///
93 /// # Returns
94 ///
95 /// Platform-specific process exit status reported by the operating system.
96 #[inline]
97 pub const fn exit_status(&self) -> &ExitStatus {
98 &self.status
99 }
100
101 /// Returns the signal that terminated the process on Unix platforms.
102 ///
103 /// # Returns
104 ///
105 /// `Some(signal)` when the process was terminated by a signal, otherwise
106 /// `None`.
107 #[cfg(unix)]
108 #[inline]
109 pub fn termination_signal(&self) -> Option<i32> {
110 self.status.signal()
111 }
112
113 /// Returns captured standard output bytes.
114 ///
115 /// # Returns
116 ///
117 /// A borrowed slice containing stdout exactly as emitted by the process.
118 #[inline]
119 pub fn stdout(&self) -> &[u8] {
120 &self.stdout
121 }
122
123 /// Returns captured standard error bytes.
124 ///
125 /// # Returns
126 ///
127 /// A borrowed slice containing stderr exactly as emitted by the process.
128 #[inline]
129 pub fn stderr(&self) -> &[u8] {
130 &self.stderr
131 }
132
133 /// Returns captured standard output as strict UTF-8 text.
134 ///
135 /// # Returns
136 ///
137 /// `Ok(&str)` when stdout is valid UTF-8.
138 ///
139 /// # Errors
140 ///
141 /// Returns [`str::Utf8Error`] when stdout contains invalid UTF-8.
142 #[inline]
143 pub fn stdout_text(&self) -> Result<&str, str::Utf8Error> {
144 str::from_utf8(&self.stdout)
145 }
146
147 /// Returns captured standard error as strict UTF-8 text.
148 ///
149 /// # Returns
150 ///
151 /// `Ok(&str)` when stderr is valid UTF-8.
152 ///
153 /// # Errors
154 ///
155 /// Returns [`str::Utf8Error`] when stderr contains invalid UTF-8.
156 #[inline]
157 pub fn stderr_text(&self) -> Result<&str, str::Utf8Error> {
158 str::from_utf8(&self.stderr)
159 }
160
161 /// Returns captured standard output as UTF-8 text, replacing invalid bytes.
162 ///
163 /// # Returns
164 ///
165 /// Borrowed UTF-8 text when stdout is valid UTF-8, or an owned string with
166 /// invalid byte sequences replaced by the Unicode replacement character.
167 #[inline]
168 pub fn stdout_lossy_text(&self) -> Cow<'_, str> {
169 String::from_utf8_lossy(&self.stdout)
170 }
171
172 /// Returns captured standard error as UTF-8 text, replacing invalid bytes.
173 ///
174 /// # Returns
175 ///
176 /// Borrowed UTF-8 text when stderr is valid UTF-8, or an owned string with
177 /// invalid byte sequences replaced by the Unicode replacement character.
178 #[inline]
179 pub fn stderr_lossy_text(&self) -> Cow<'_, str> {
180 String::from_utf8_lossy(&self.stderr)
181 }
182
183 /// Returns the observed command duration.
184 ///
185 /// # Returns
186 ///
187 /// Duration from process spawn to observed termination.
188 #[inline]
189 pub const fn elapsed(&self) -> Duration {
190 self.elapsed
191 }
192
193 /// Returns whether captured stdout was truncated by a configured limit.
194 ///
195 /// # Returns
196 ///
197 /// `true` when stdout emitted more bytes than the runner retained.
198 #[inline]
199 pub const fn stdout_truncated(&self) -> bool {
200 self.stdout_truncated
201 }
202
203 /// Returns whether captured stderr was truncated by a configured limit.
204 ///
205 /// # Returns
206 ///
207 /// `true` when stderr emitted more bytes than the runner retained.
208 #[inline]
209 pub const fn stderr_truncated(&self) -> bool {
210 self.stderr_truncated
211 }
212}