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