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