1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4use std::ffi::OsStr;
5use std::marker::PhantomData;
6
7#[derive(Debug, thiserror::Error)]
8pub enum CommandError {
9 #[error("command IO failure")]
10 Io(#[source] std::io::Error),
11 #[error("command exited with {0}")]
12 ExitStatus(std::process::ExitStatus),
13}
14
15async fn write_stdin(
16 child: &mut tokio::process::Child,
17 stdin_data: Option<Vec<u8>>,
18) -> Result<(), CommandError> {
19 use tokio::io::AsyncWriteExt;
20
21 if let Some(data) = stdin_data {
22 child
23 .stdin
24 .take()
25 .unwrap()
26 .write_all(&data)
27 .await
28 .map_err(CommandError::Io)?;
29 }
30
31 Ok(())
32}
33
34async fn run_and_wait(
35 mut child: tokio::process::Child,
36 stdin_data: Option<Vec<u8>>,
37 start: std::time::Instant,
38) -> Result<std::process::Output, CommandError> {
39 write_stdin(&mut child, stdin_data).await?;
40
41 let output = child.wait_with_output().await.map_err(CommandError::Io)?;
42
43 log::debug!(
44 "exit_status={:?} runtime={:?}",
45 output.status,
46 start.elapsed()
47 );
48
49 Ok(output)
50}
51
52async fn run_and_wait_status(
53 mut child: tokio::process::Child,
54 stdin_data: Option<Vec<u8>>,
55 start: std::time::Instant,
56) -> Result<std::process::ExitStatus, CommandError> {
57 write_stdin(&mut child, stdin_data).await?;
58
59 let status = child.wait().await.map_err(CommandError::Io)?;
60
61 log::debug!("exit_status={:?} runtime={:?}", status, start.elapsed());
62
63 Ok(status)
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
70pub struct EnvVariableName(Cow<'static, str>);
71
72impl EnvVariableName {
73 #[must_use]
74 pub fn as_str(&self) -> &str {
75 &self.0
76 }
77
78 #[must_use]
85 pub const fn from_static_or_panic(name: &'static str) -> Self {
86 match validate_env_variable_name(name) {
87 Ok(()) => {}
88 Err(EnvVariableNameError::Empty) => {
89 panic!("Environment variable name cannot be empty");
90 }
91 Err(EnvVariableNameError::ContainsEquals) => {
92 panic!("Environment variable name cannot contain '='");
93 }
94 }
95 Self(Cow::Borrowed(name))
96 }
97}
98
99impl AsRef<OsStr> for EnvVariableName {
100 fn as_ref(&self) -> &OsStr {
101 self.0.as_ref().as_ref()
102 }
103}
104
105impl std::fmt::Display for EnvVariableName {
106 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 formatter.write_str(self.as_str())
108 }
109}
110
111#[derive(Debug, thiserror::Error)]
112pub enum EnvVariableNameError {
113 #[error("Environment variable name cannot be empty")]
114 Empty,
115 #[error("Environment variable name cannot contain '='")]
116 ContainsEquals,
117}
118
119impl std::str::FromStr for EnvVariableName {
120 type Err = EnvVariableNameError;
121
122 fn from_str(name: &str) -> Result<Self, Self::Err> {
123 validate_env_variable_name(name).map(|()| Self(Cow::Owned(name.to_string())))
124 }
125}
126
127const fn validate_env_variable_name(s: &str) -> Result<(), EnvVariableNameError> {
128 if s.is_empty() {
129 return Err(EnvVariableNameError::Empty);
130 }
131 let bytes = s.as_bytes();
132 let mut index = 0;
133 while index < bytes.len() {
135 if bytes[index] == b'=' {
136 return Err(EnvVariableNameError::ContainsEquals);
137 }
138 index += 1;
139 }
140 Ok(())
141}
142
143const _: () = assert!(usize::BITS >= u16::BITS);
144
145pub const ENV_VARIABLE_VALUE_MAX_LEN: usize = u16::MAX as usize;
149
150#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
155pub struct EnvVariableValue(Cow<'static, str>);
156
157impl EnvVariableValue {
158 #[must_use]
159 pub fn as_str(&self) -> &str {
160 &self.0
161 }
162
163 #[must_use]
170 pub const fn from_static_or_panic(value: &'static str) -> Self {
171 match validate_env_variable_value(value) {
172 Ok(()) => {}
173 Err(EnvVariableValueError::ContainsNul { .. }) => {
174 panic!("Environment variable value cannot contain NUL byte");
175 }
176 Err(EnvVariableValueError::TooLong { .. }) => {
177 panic!("Environment variable value exceeds maximum of 65535 bytes");
178 }
179 }
180 Self(Cow::Borrowed(value))
181 }
182}
183
184impl AsRef<OsStr> for EnvVariableValue {
185 fn as_ref(&self) -> &OsStr {
186 self.0.as_ref().as_ref()
187 }
188}
189
190impl AsRef<str> for EnvVariableValue {
191 fn as_ref(&self) -> &str {
192 &self.0
193 }
194}
195
196impl std::fmt::Display for EnvVariableValue {
197 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 formatter.write_str(self.as_str())
199 }
200}
201
202#[derive(Debug, thiserror::Error)]
203pub enum EnvVariableValueError {
204 #[error("Environment variable value contains NUL byte at index {index}")]
205 ContainsNul { index: usize },
206 #[error("Environment variable value length {length} exceeds maximum {max}")]
207 TooLong { length: usize, max: usize },
208}
209
210impl std::str::FromStr for EnvVariableValue {
211 type Err = EnvVariableValueError;
212
213 fn from_str(value: &str) -> Result<Self, Self::Err> {
214 validate_env_variable_value(value).map(|()| Self(Cow::Owned(value.to_string())))
215 }
216}
217
218impl TryFrom<String> for EnvVariableValue {
219 type Error = EnvVariableValueError;
220
221 fn try_from(value: String) -> Result<Self, Self::Error> {
222 validate_env_variable_value(&value).map(|()| Self(Cow::Owned(value)))
223 }
224}
225
226impl From<&'static str> for EnvVariableValue {
227 fn from(value: &'static str) -> Self {
228 Self::from_static_or_panic(value)
229 }
230}
231
232const fn validate_env_variable_value(value: &str) -> Result<(), EnvVariableValueError> {
233 let bytes = value.as_bytes();
234 if bytes.len() > ENV_VARIABLE_VALUE_MAX_LEN {
235 return Err(EnvVariableValueError::TooLong {
236 length: bytes.len(),
237 max: ENV_VARIABLE_VALUE_MAX_LEN,
238 });
239 }
240 let mut index = 0;
241 while index < bytes.len() {
242 if bytes[index] == 0 {
243 return Err(EnvVariableValueError::ContainsNul { index });
244 }
245 index += 1;
246 }
247 Ok(())
248}
249
250mod sealed {
251 pub trait Sealed {}
252}
253
254pub trait StreamMarker: sealed::Sealed {}
256
257pub struct Stdout;
259
260pub struct Stderr;
262
263impl sealed::Sealed for Stdout {}
264impl sealed::Sealed for Stderr {}
265impl StreamMarker for Stdout {}
266impl StreamMarker for Stderr {}
267
268#[derive(Debug)]
270pub struct CaptureSingleResult {
271 pub bytes: Vec<u8>,
272 pub status: std::process::ExitStatus,
273}
274
275#[derive(Debug)]
277pub struct CaptureAllResult {
278 pub stdout: Vec<u8>,
279 pub stderr: Vec<u8>,
280 pub status: std::process::ExitStatus,
281}
282
283async fn run_capture(
284 mut command: Command,
285 accept_nonzero_exit: bool,
286) -> Result<std::process::Output, CommandError> {
287 log::debug!("{:#?}", command.inner);
288
289 if command.stdin_data.is_some() {
290 command.inner.stdin(std::process::Stdio::piped());
291 }
292
293 let start = std::time::Instant::now();
294
295 let child = command.inner.spawn().map_err(CommandError::Io)?;
296
297 let output = run_and_wait(child, command.stdin_data, start).await?;
298
299 if accept_nonzero_exit || output.status.success() {
300 Ok(output)
301 } else {
302 Err(CommandError::ExitStatus(output.status))
303 }
304}
305
306pub struct CaptureSingle<S: StreamMarker> {
311 inner: tokio::process::Command,
312 stdin_data: Option<Vec<u8>>,
313 accept_nonzero_exit: bool,
314 _marker: PhantomData<S>,
315}
316
317impl<S: StreamMarker> CaptureSingle<S> {
318 #[must_use]
320 pub fn accept_nonzero_exit(mut self) -> Self {
321 self.accept_nonzero_exit = true;
322 self
323 }
324}
325
326impl CaptureSingle<Stdout> {
327 #[must_use]
329 pub fn stderr_capture(mut self) -> CaptureAll {
330 self.inner.stdout(std::process::Stdio::piped());
331 self.inner.stderr(std::process::Stdio::piped());
332 CaptureAll {
333 inner: self.inner,
334 stdin_data: self.stdin_data,
335 accept_nonzero_exit: self.accept_nonzero_exit,
336 }
337 }
338
339 #[must_use]
341 pub fn stderr_null(mut self) -> Self {
342 self.inner.stderr(std::process::Stdio::null());
343 self
344 }
345
346 #[must_use]
348 pub fn stderr_inherit(mut self) -> Self {
349 self.inner.stderr(std::process::Stdio::inherit());
350 self
351 }
352
353 #[must_use]
355 pub fn stdout_null(mut self) -> Command {
356 self.inner.stdout(std::process::Stdio::null());
357 Command {
358 inner: self.inner,
359 stdin_data: self.stdin_data,
360 }
361 }
362
363 #[must_use]
365 pub fn stdout_inherit(mut self) -> Command {
366 self.inner.stdout(std::process::Stdio::inherit());
367 Command {
368 inner: self.inner,
369 stdin_data: self.stdin_data,
370 }
371 }
372
373 pub async fn run(mut self) -> Result<CaptureSingleResult, CommandError> {
375 self.inner.stdout(std::process::Stdio::piped());
376
377 let command = Command {
378 inner: self.inner,
379 stdin_data: self.stdin_data,
380 };
381
382 let output = run_capture(command, self.accept_nonzero_exit).await?;
383
384 Ok(CaptureSingleResult {
385 bytes: output.stdout,
386 status: output.status,
387 })
388 }
389
390 pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
392 Ok(self.run().await?.bytes)
393 }
394
395 pub async fn string(self) -> Result<String, CommandError> {
397 let bytes = self.bytes().await?;
398 String::from_utf8(bytes).map_err(|utf8_error| {
399 CommandError::Io(std::io::Error::new(
400 std::io::ErrorKind::InvalidData,
401 utf8_error,
402 ))
403 })
404 }
405}
406
407impl CaptureSingle<Stderr> {
408 #[must_use]
410 pub fn stdout_capture(mut self) -> CaptureAll {
411 self.inner.stdout(std::process::Stdio::piped());
412 self.inner.stderr(std::process::Stdio::piped());
413 CaptureAll {
414 inner: self.inner,
415 stdin_data: self.stdin_data,
416 accept_nonzero_exit: self.accept_nonzero_exit,
417 }
418 }
419
420 #[must_use]
422 pub fn stdout_null(mut self) -> Self {
423 self.inner.stdout(std::process::Stdio::null());
424 self
425 }
426
427 #[must_use]
429 pub fn stdout_inherit(mut self) -> Self {
430 self.inner.stdout(std::process::Stdio::inherit());
431 self
432 }
433
434 #[must_use]
436 pub fn stderr_null(mut self) -> Command {
437 self.inner.stderr(std::process::Stdio::null());
438 Command {
439 inner: self.inner,
440 stdin_data: self.stdin_data,
441 }
442 }
443
444 #[must_use]
446 pub fn stderr_inherit(mut self) -> Command {
447 self.inner.stderr(std::process::Stdio::inherit());
448 Command {
449 inner: self.inner,
450 stdin_data: self.stdin_data,
451 }
452 }
453
454 pub async fn run(mut self) -> Result<CaptureSingleResult, CommandError> {
456 self.inner.stderr(std::process::Stdio::piped());
457
458 let command = Command {
459 inner: self.inner,
460 stdin_data: self.stdin_data,
461 };
462
463 let output = run_capture(command, self.accept_nonzero_exit).await?;
464
465 Ok(CaptureSingleResult {
466 bytes: output.stderr,
467 status: output.status,
468 })
469 }
470
471 pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
473 Ok(self.run().await?.bytes)
474 }
475
476 pub async fn string(self) -> Result<String, CommandError> {
478 let bytes = self.bytes().await?;
479 String::from_utf8(bytes).map_err(|utf8_error| {
480 CommandError::Io(std::io::Error::new(
481 std::io::ErrorKind::InvalidData,
482 utf8_error,
483 ))
484 })
485 }
486}
487
488pub struct CaptureAll {
490 inner: tokio::process::Command,
491 stdin_data: Option<Vec<u8>>,
492 accept_nonzero_exit: bool,
493}
494
495impl CaptureAll {
496 #[must_use]
498 pub fn accept_nonzero_exit(mut self) -> Self {
499 self.accept_nonzero_exit = true;
500 self
501 }
502
503 #[must_use]
505 pub fn stdout_null(mut self) -> CaptureSingle<Stderr> {
506 self.inner.stdout(std::process::Stdio::null());
507 CaptureSingle {
508 inner: self.inner,
509 stdin_data: self.stdin_data,
510 accept_nonzero_exit: self.accept_nonzero_exit,
511 _marker: PhantomData,
512 }
513 }
514
515 #[must_use]
517 pub fn stdout_inherit(mut self) -> CaptureSingle<Stderr> {
518 self.inner.stdout(std::process::Stdio::inherit());
519 CaptureSingle {
520 inner: self.inner,
521 stdin_data: self.stdin_data,
522 accept_nonzero_exit: self.accept_nonzero_exit,
523 _marker: PhantomData,
524 }
525 }
526
527 #[must_use]
529 pub fn stderr_null(mut self) -> CaptureSingle<Stdout> {
530 self.inner.stderr(std::process::Stdio::null());
531 CaptureSingle {
532 inner: self.inner,
533 stdin_data: self.stdin_data,
534 accept_nonzero_exit: self.accept_nonzero_exit,
535 _marker: PhantomData,
536 }
537 }
538
539 #[must_use]
541 pub fn stderr_inherit(mut self) -> CaptureSingle<Stdout> {
542 self.inner.stderr(std::process::Stdio::inherit());
543 CaptureSingle {
544 inner: self.inner,
545 stdin_data: self.stdin_data,
546 accept_nonzero_exit: self.accept_nonzero_exit,
547 _marker: PhantomData,
548 }
549 }
550
551 pub async fn run(mut self) -> Result<CaptureAllResult, CommandError> {
553 self.inner.stdout(std::process::Stdio::piped());
554 self.inner.stderr(std::process::Stdio::piped());
555
556 let command = Command {
557 inner: self.inner,
558 stdin_data: self.stdin_data,
559 };
560
561 let output = run_capture(command, self.accept_nonzero_exit).await?;
562
563 Ok(CaptureAllResult {
564 stdout: output.stdout,
565 stderr: output.stderr,
566 status: output.status,
567 })
568 }
569}
570
571pub struct Command {
572 inner: tokio::process::Command,
573 stdin_data: Option<Vec<u8>>,
574}
575
576impl Command {
577 pub fn new(value: impl AsRef<OsStr>) -> Self {
578 Command {
579 inner: tokio::process::Command::new(value),
580 stdin_data: None,
581 }
582 }
583
584 #[cfg(feature = "test-utils")]
593 pub fn test_eq(&self, other: &Self) {
594 assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
595 }
596
597 pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
598 self.inner.arg(value);
599 self
600 }
601
602 pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
603 if let Some(value) = optional {
604 self.inner.arg(value);
605 }
606 self
607 }
608
609 pub fn optional_flag(mut self, condition: bool, flag: impl AsRef<OsStr>) -> Self {
617 if condition {
618 self.inner.arg(flag);
619 }
620 self
621 }
622
623 pub fn option(mut self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
631 self.inner.arg(name);
632 self.inner.arg(value);
633 self
634 }
635
636 pub fn optional_option(
644 mut self,
645 name: impl AsRef<OsStr>,
646 value: Option<impl AsRef<OsStr>>,
647 ) -> Self {
648 if let Some(value) = value {
649 self.inner.arg(name);
650 self.inner.arg(value);
651 }
652 self
653 }
654
655 pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
656 self.inner.args(value);
657 self
658 }
659
660 pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
661 self.inner.current_dir(dir);
662 self
663 }
664
665 pub fn env(mut self, key: &EnvVariableName, val: impl AsRef<OsStr>) -> Self {
666 self.inner.env(key, val);
667 self
668 }
669
670 pub fn envs<I, V>(mut self, vars: I) -> Self
671 where
672 I: IntoIterator<Item = (EnvVariableName, V)>,
673 V: AsRef<OsStr>,
674 {
675 for (key, val) in vars {
676 self.inner.env(key, val);
677 }
678 self
679 }
680
681 #[must_use]
683 pub fn env_remove(mut self, key: &EnvVariableName) -> Self {
684 self.inner.env_remove(key);
685 self
686 }
687
688 #[must_use]
689 pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
690 self.stdin_data = Some(data.into());
691 self
692 }
693
694 #[must_use]
696 pub fn stdout_capture(self) -> CaptureSingle<Stdout> {
697 CaptureSingle {
698 inner: self.inner,
699 stdin_data: self.stdin_data,
700 accept_nonzero_exit: false,
701 _marker: PhantomData,
702 }
703 }
704
705 #[must_use]
707 pub fn stderr_capture(self) -> CaptureSingle<Stderr> {
708 CaptureSingle {
709 inner: self.inner,
710 stdin_data: self.stdin_data,
711 accept_nonzero_exit: false,
712 _marker: PhantomData,
713 }
714 }
715
716 #[must_use]
718 pub fn stdout_null(mut self) -> Self {
719 self.inner.stdout(std::process::Stdio::null());
720 self
721 }
722
723 #[must_use]
725 pub fn stderr_null(mut self) -> Self {
726 self.inner.stderr(std::process::Stdio::null());
727 self
728 }
729
730 #[must_use]
732 pub fn stdout_inherit(mut self) -> Self {
733 self.inner.stdout(std::process::Stdio::inherit());
734 self
735 }
736
737 #[must_use]
739 pub fn stderr_inherit(mut self) -> Self {
740 self.inner.stderr(std::process::Stdio::inherit());
741 self
742 }
743
744 #[must_use]
749 pub fn build(self) -> tokio::process::Command {
750 self.inner
751 }
752
753 pub async fn status(mut self) -> Result<(), CommandError> {
755 use std::process::Stdio;
756
757 log::debug!("{:#?}", self.inner);
758
759 if self.stdin_data.is_some() {
760 self.inner.stdin(Stdio::piped());
761 }
762
763 let start = std::time::Instant::now();
764
765 let child = self.inner.spawn().map_err(CommandError::Io)?;
766
767 let exit_status = run_and_wait_status(child, self.stdin_data, start).await?;
768
769 if exit_status.success() {
770 Ok(())
771 } else {
772 Err(CommandError::ExitStatus(exit_status))
773 }
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[tokio::test]
782 async fn test_stdout_bytes_success() {
783 assert_eq!(
784 Command::new("echo")
785 .argument("hello")
786 .stdout_capture()
787 .bytes()
788 .await
789 .unwrap(),
790 b"hello\n"
791 );
792 }
793
794 #[tokio::test]
795 async fn test_stdout_bytes_nonzero_exit() {
796 let error = Command::new("sh")
797 .arguments(["-c", "exit 42"])
798 .stdout_capture()
799 .bytes()
800 .await
801 .unwrap_err();
802 let CommandError::ExitStatus(status) = error else {
803 panic!("expected ExitStatus, got {error:?}");
804 };
805 assert_eq!(status.code(), Some(42));
806 }
807
808 #[tokio::test]
809 async fn test_stdout_bytes_io_error() {
810 let error = Command::new("./nonexistent")
811 .stdout_capture()
812 .bytes()
813 .await
814 .unwrap_err();
815 let CommandError::Io(io_error) = error else {
816 panic!("expected Io, got {error:?}");
817 };
818 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
819 }
820
821 #[tokio::test]
822 async fn test_stdout_string_success() {
823 assert_eq!(
824 Command::new("echo")
825 .argument("hello")
826 .stdout_capture()
827 .string()
828 .await
829 .unwrap(),
830 "hello\n"
831 );
832 }
833
834 #[tokio::test]
835 async fn test_stderr_bytes_success() {
836 assert_eq!(
837 Command::new("sh")
838 .arguments(["-c", "echo error >&2"])
839 .stderr_capture()
840 .bytes()
841 .await
842 .unwrap(),
843 b"error\n"
844 );
845 }
846
847 #[tokio::test]
848 async fn test_stderr_string_success() {
849 assert_eq!(
850 Command::new("sh")
851 .arguments(["-c", "echo error >&2"])
852 .stderr_capture()
853 .string()
854 .await
855 .unwrap(),
856 "error\n"
857 );
858 }
859
860 #[tokio::test]
861 async fn test_status_success() {
862 assert!(Command::new("true").status().await.is_ok());
863 }
864
865 #[tokio::test]
866 async fn test_status_nonzero_exit() {
867 let error = Command::new("sh")
868 .arguments(["-c", "exit 42"])
869 .status()
870 .await
871 .unwrap_err();
872 let CommandError::ExitStatus(status) = error else {
873 panic!("expected ExitStatus, got {error:?}");
874 };
875 assert_eq!(status.code(), Some(42));
876 }
877
878 #[tokio::test]
879 async fn test_status_io_error() {
880 let error = Command::new("./nonexistent").status().await.unwrap_err();
881 let CommandError::Io(io_error) = error else {
882 panic!("expected Io, got {error:?}");
883 };
884 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
885 }
886
887 #[test]
888 fn test_env_variable_name_from_static_or_panic() {
889 const NAME: EnvVariableName = EnvVariableName::from_static_or_panic("PATH");
890 assert_eq!(NAME.as_str(), "PATH");
891 }
892
893 #[test]
894 fn test_env_variable_name_parse() {
895 let name: EnvVariableName = "HOME".parse().unwrap();
896 assert_eq!(name.as_str(), "HOME");
897 }
898
899 #[test]
900 fn test_env_variable_name_empty() {
901 let result: Result<EnvVariableName, _> = "".parse();
902 assert!(matches!(result, Err(EnvVariableNameError::Empty)));
903 }
904
905 #[test]
906 fn test_env_variable_name_contains_equals() {
907 let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
908 assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
909 }
910
911 #[tokio::test]
912 async fn test_env_with_variable() {
913 let name: EnvVariableName = "MY_VAR".parse().unwrap();
914 let output = Command::new("sh")
915 .arguments(["-c", "echo $MY_VAR"])
916 .env(&name, "hello")
917 .stdout_capture()
918 .string()
919 .await
920 .unwrap();
921 assert_eq!(output, "hello\n");
922 }
923
924 #[tokio::test]
925 async fn test_stdin_bytes() {
926 let output = Command::new("cat")
927 .stdin_bytes(b"hello world".as_slice())
928 .stdout_capture()
929 .string()
930 .await
931 .unwrap();
932 assert_eq!(output, "hello world");
933 }
934
935 #[tokio::test]
936 async fn test_stdin_bytes_vec() {
937 let output = Command::new("cat")
938 .stdin_bytes(vec![104, 105])
939 .stdout_capture()
940 .string()
941 .await
942 .unwrap();
943 assert_eq!(output, "hi");
944 }
945
946 #[tokio::test]
947 async fn test_capture_all_success() {
948 let result = Command::new("echo")
949 .argument("hello")
950 .stdout_capture()
951 .stderr_capture()
952 .run()
953 .await
954 .unwrap();
955 assert!(result.status.success());
956 assert_eq!(result.stdout, b"hello\n");
957 assert!(result.stderr.is_empty());
958 }
959
960 #[tokio::test]
961 async fn test_capture_all_failure_with_stderr() {
962 let result = Command::new("sh")
963 .arguments(["-c", "echo error >&2; exit 1"])
964 .stdout_capture()
965 .stderr_capture()
966 .accept_nonzero_exit()
967 .run()
968 .await
969 .unwrap();
970 assert!(!result.status.success());
971 assert_eq!(String::from_utf8(result.stderr).unwrap(), "error\n");
972 }
973
974 #[tokio::test]
975 async fn test_capture_all_io_error() {
976 let error = Command::new("./nonexistent")
977 .stdout_capture()
978 .stderr_capture()
979 .run()
980 .await
981 .unwrap_err();
982 let CommandError::Io(io_error) = error else {
983 panic!("expected Io, got {error:?}");
984 };
985 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
986 }
987
988 #[tokio::test]
989 async fn test_build() {
990 use tokio::io::{AsyncReadExt, AsyncWriteExt};
991
992 let mut child = Command::new("cat")
993 .build()
994 .stdin(std::process::Stdio::piped())
995 .stdout(std::process::Stdio::piped())
996 .spawn()
997 .unwrap();
998
999 child
1000 .stdin
1001 .as_mut()
1002 .unwrap()
1003 .write_all(b"hello")
1004 .await
1005 .unwrap();
1006 drop(child.stdin.take());
1007
1008 let mut output = String::new();
1009 child
1010 .stdout
1011 .as_mut()
1012 .unwrap()
1013 .read_to_string(&mut output)
1014 .await
1015 .unwrap();
1016 assert_eq!(output, "hello");
1017
1018 let status = child.wait().await.unwrap();
1019 assert!(status.success());
1020 }
1021
1022 #[tokio::test]
1023 async fn test_option() {
1024 let output = Command::new("echo")
1025 .option("-n", "hello")
1026 .stdout_capture()
1027 .string()
1028 .await
1029 .unwrap();
1030 assert_eq!(output, "hello");
1031 }
1032
1033 #[tokio::test]
1034 async fn test_optional_option_some() {
1035 let output = Command::new("echo")
1036 .optional_option("-n", Some("hello"))
1037 .stdout_capture()
1038 .string()
1039 .await
1040 .unwrap();
1041 assert_eq!(output, "hello");
1042 }
1043
1044 #[tokio::test]
1045 async fn test_optional_option_none() {
1046 let output = Command::new("echo")
1047 .optional_option("-n", None::<&str>)
1048 .argument("hello")
1049 .stdout_capture()
1050 .string()
1051 .await
1052 .unwrap();
1053 assert_eq!(output, "hello\n");
1054 }
1055
1056 #[tokio::test]
1057 async fn test_optional_flag_true() {
1058 let output = Command::new("echo")
1059 .optional_flag(true, "-n")
1060 .argument("hello")
1061 .stdout_capture()
1062 .string()
1063 .await
1064 .unwrap();
1065 assert_eq!(output, "hello");
1066 }
1067
1068 #[tokio::test]
1069 async fn test_optional_flag_false() {
1070 let output = Command::new("echo")
1071 .optional_flag(false, "-n")
1072 .argument("hello")
1073 .stdout_capture()
1074 .string()
1075 .await
1076 .unwrap();
1077 assert_eq!(output, "hello\n");
1078 }
1079
1080 #[tokio::test]
1081 async fn test_stdout_null() {
1082 Command::new("echo")
1084 .argument("hello")
1085 .stdout_null()
1086 .status()
1087 .await
1088 .unwrap();
1089 }
1090
1091 #[tokio::test]
1092 async fn test_stderr_null() {
1093 Command::new("sh")
1095 .arguments(["-c", "echo error >&2"])
1096 .stderr_null()
1097 .status()
1098 .await
1099 .unwrap();
1100 }
1101
1102 #[tokio::test]
1103 async fn test_stdout_capture_stderr_null() {
1104 let output = Command::new("sh")
1105 .arguments(["-c", "echo out; echo err >&2"])
1106 .stdout_capture()
1107 .stderr_null()
1108 .string()
1109 .await
1110 .unwrap();
1111 assert_eq!(output, "out\n");
1112 }
1113
1114 #[tokio::test]
1115 async fn test_accept_nonzero_exit_stdout() {
1116 let result = Command::new("sh")
1117 .arguments(["-c", "echo out; exit 42"])
1118 .stdout_capture()
1119 .accept_nonzero_exit()
1120 .run()
1121 .await
1122 .unwrap();
1123 assert!(!result.status.success());
1124 assert_eq!(result.bytes, b"out\n");
1125 }
1126
1127 #[tokio::test]
1128 async fn test_accept_nonzero_exit_capture_all() {
1129 let result = Command::new("sh")
1130 .arguments(["-c", "echo out; echo err >&2; exit 42"])
1131 .stdout_capture()
1132 .stderr_capture()
1133 .accept_nonzero_exit()
1134 .run()
1135 .await
1136 .unwrap();
1137 assert!(!result.status.success());
1138 assert_eq!(result.stdout, b"out\n");
1139 assert_eq!(result.stderr, b"err\n");
1140 }
1141}