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 pub fn read(&self) -> Result<EnvVariableValue, EnvVariableReadError> {
106 match std::env::var(self.as_str()) {
107 Ok(value) => EnvVariableValue::try_from(value).map_err(|source| {
108 EnvVariableReadError::InvalidValue {
109 name: self.clone(),
110 source,
111 }
112 }),
113 Err(std::env::VarError::NotPresent) => {
114 Err(EnvVariableReadError::NotPresent { name: self.clone() })
115 }
116 Err(std::env::VarError::NotUnicode(_)) => {
117 Err(EnvVariableReadError::NotUnicode { name: self.clone() })
118 }
119 }
120 }
121
122 #[must_use]
128 pub fn is_present(&self) -> bool {
129 std::env::var_os(self.as_str()).is_some()
130 }
131
132 pub fn load_from_str<T>(&self) -> Result<T, EnvVariableLoadError<T::Err>>
140 where
141 T: std::str::FromStr,
142 T::Err: std::error::Error + 'static,
143 {
144 let value = self.read()?;
145 value
146 .as_str()
147 .parse()
148 .map_err(|source| EnvVariableLoadError::Convert {
149 name: self.clone(),
150 source,
151 })
152 }
153
154 pub fn load_try_from<T>(&self) -> Result<T, EnvVariableLoadError<T::Error>>
162 where
163 T: TryFrom<String>,
164 T::Error: std::error::Error + 'static,
165 {
166 let value = self.read()?;
167 T::try_from(value.as_str().to_owned()).map_err(|source| EnvVariableLoadError::Convert {
168 name: self.clone(),
169 source,
170 })
171 }
172}
173
174impl AsRef<OsStr> for EnvVariableName {
175 fn as_ref(&self) -> &OsStr {
176 self.0.as_ref().as_ref()
177 }
178}
179
180impl std::fmt::Display for EnvVariableName {
181 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 formatter.write_str(self.as_str())
183 }
184}
185
186#[derive(Debug, thiserror::Error)]
187pub enum EnvVariableNameError {
188 #[error("Environment variable name cannot be empty")]
189 Empty,
190 #[error("Environment variable name cannot contain '='")]
191 ContainsEquals,
192}
193
194impl std::str::FromStr for EnvVariableName {
195 type Err = EnvVariableNameError;
196
197 fn from_str(name: &str) -> Result<Self, Self::Err> {
198 validate_env_variable_name(name).map(|()| Self(Cow::Owned(name.to_string())))
199 }
200}
201
202const fn validate_env_variable_name(s: &str) -> Result<(), EnvVariableNameError> {
203 if s.is_empty() {
204 return Err(EnvVariableNameError::Empty);
205 }
206 let bytes = s.as_bytes();
207 let mut index = 0;
208 while index < bytes.len() {
210 if bytes[index] == b'=' {
211 return Err(EnvVariableNameError::ContainsEquals);
212 }
213 index += 1;
214 }
215 Ok(())
216}
217
218const _: () = assert!(usize::BITS >= u16::BITS);
219
220pub const ENV_VARIABLE_VALUE_MAX_LEN: usize = u16::MAX as usize;
224
225#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
230pub struct EnvVariableValue(Cow<'static, str>);
231
232impl EnvVariableValue {
233 #[must_use]
234 pub fn as_str(&self) -> &str {
235 &self.0
236 }
237
238 #[must_use]
245 pub const fn from_static_or_panic(value: &'static str) -> Self {
246 match validate_env_variable_value(value) {
247 Ok(()) => {}
248 Err(EnvVariableValueError::ContainsNul { .. }) => {
249 panic!("Environment variable value cannot contain NUL byte");
250 }
251 Err(EnvVariableValueError::TooLong { .. }) => {
252 panic!("Environment variable value exceeds maximum of 65535 bytes");
253 }
254 }
255 Self(Cow::Borrowed(value))
256 }
257}
258
259impl AsRef<OsStr> for EnvVariableValue {
260 fn as_ref(&self) -> &OsStr {
261 self.0.as_ref().as_ref()
262 }
263}
264
265impl AsRef<str> for EnvVariableValue {
266 fn as_ref(&self) -> &str {
267 &self.0
268 }
269}
270
271impl std::fmt::Display for EnvVariableValue {
272 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 formatter.write_str(self.as_str())
274 }
275}
276
277#[derive(Debug, thiserror::Error)]
278pub enum EnvVariableValueError {
279 #[error("Environment variable value contains NUL byte at index {index}")]
280 ContainsNul { index: usize },
281 #[error("Environment variable value length {length} exceeds maximum {max}")]
282 TooLong { length: usize, max: usize },
283}
284
285#[derive(Debug, thiserror::Error)]
287pub enum EnvVariableReadError {
288 #[error("environment variable {name} is not present")]
289 NotPresent { name: EnvVariableName },
290 #[error("environment variable {name} value is not valid unicode")]
291 NotUnicode { name: EnvVariableName },
292 #[error("environment variable {name} value is invalid")]
293 InvalidValue {
294 name: EnvVariableName,
295 #[source]
296 source: EnvVariableValueError,
297 },
298}
299
300#[derive(Debug, thiserror::Error)]
303pub enum EnvVariableLoadError<E> {
304 #[error(transparent)]
305 Read(#[from] EnvVariableReadError),
306 #[error("environment variable {name} value could not be converted")]
307 Convert {
308 name: EnvVariableName,
309 #[source]
310 source: E,
311 },
312}
313
314impl std::str::FromStr for EnvVariableValue {
315 type Err = EnvVariableValueError;
316
317 fn from_str(value: &str) -> Result<Self, Self::Err> {
318 validate_env_variable_value(value).map(|()| Self(Cow::Owned(value.to_string())))
319 }
320}
321
322impl TryFrom<String> for EnvVariableValue {
323 type Error = EnvVariableValueError;
324
325 fn try_from(value: String) -> Result<Self, Self::Error> {
326 validate_env_variable_value(&value).map(|()| Self(Cow::Owned(value)))
327 }
328}
329
330impl From<&'static str> for EnvVariableValue {
331 fn from(value: &'static str) -> Self {
332 Self::from_static_or_panic(value)
333 }
334}
335
336const fn validate_env_variable_value(value: &str) -> Result<(), EnvVariableValueError> {
337 let bytes = value.as_bytes();
338 if bytes.len() > ENV_VARIABLE_VALUE_MAX_LEN {
339 return Err(EnvVariableValueError::TooLong {
340 length: bytes.len(),
341 max: ENV_VARIABLE_VALUE_MAX_LEN,
342 });
343 }
344 let mut index = 0;
345 while index < bytes.len() {
346 if bytes[index] == 0 {
347 return Err(EnvVariableValueError::ContainsNul { index });
348 }
349 index += 1;
350 }
351 Ok(())
352}
353
354mod sealed {
355 pub trait Sealed {}
356}
357
358pub trait StreamMarker: sealed::Sealed {}
360
361pub struct Stdout;
363
364pub struct Stderr;
366
367impl sealed::Sealed for Stdout {}
368impl sealed::Sealed for Stderr {}
369impl StreamMarker for Stdout {}
370impl StreamMarker for Stderr {}
371
372#[derive(Debug)]
374pub struct CaptureSingleResult {
375 pub bytes: Vec<u8>,
376 pub status: std::process::ExitStatus,
377}
378
379#[derive(Debug)]
381pub struct CaptureAllResult {
382 pub stdout: Vec<u8>,
383 pub stderr: Vec<u8>,
384 pub status: std::process::ExitStatus,
385}
386
387async fn run_capture(
388 mut command: Command,
389 accept_nonzero_exit: bool,
390) -> Result<std::process::Output, CommandError> {
391 log::debug!("{:#?}", command.inner);
392
393 if command.stdin_data.is_some() {
394 command.inner.stdin(std::process::Stdio::piped());
395 }
396
397 let start = std::time::Instant::now();
398
399 let child = command.inner.spawn().map_err(CommandError::Io)?;
400
401 let output = run_and_wait(child, command.stdin_data, start).await?;
402
403 if accept_nonzero_exit || output.status.success() {
404 Ok(output)
405 } else {
406 Err(CommandError::ExitStatus(output.status))
407 }
408}
409
410pub struct CaptureSingle<S: StreamMarker> {
415 inner: tokio::process::Command,
416 stdin_data: Option<Vec<u8>>,
417 accept_nonzero_exit: bool,
418 _marker: PhantomData<S>,
419}
420
421impl<S: StreamMarker> CaptureSingle<S> {
422 #[must_use]
424 pub fn accept_nonzero_exit(mut self) -> Self {
425 self.accept_nonzero_exit = true;
426 self
427 }
428}
429
430impl CaptureSingle<Stdout> {
431 #[must_use]
433 pub fn stderr_capture(mut self) -> CaptureAll {
434 self.inner.stdout(std::process::Stdio::piped());
435 self.inner.stderr(std::process::Stdio::piped());
436 CaptureAll {
437 inner: self.inner,
438 stdin_data: self.stdin_data,
439 accept_nonzero_exit: self.accept_nonzero_exit,
440 }
441 }
442
443 #[must_use]
445 pub fn stderr_null(mut self) -> Self {
446 self.inner.stderr(std::process::Stdio::null());
447 self
448 }
449
450 #[must_use]
452 pub fn stderr_inherit(mut self) -> Self {
453 self.inner.stderr(std::process::Stdio::inherit());
454 self
455 }
456
457 #[must_use]
459 pub fn stdout_null(mut self) -> Command {
460 self.inner.stdout(std::process::Stdio::null());
461 Command {
462 inner: self.inner,
463 stdin_data: self.stdin_data,
464 }
465 }
466
467 #[must_use]
469 pub fn stdout_inherit(mut self) -> Command {
470 self.inner.stdout(std::process::Stdio::inherit());
471 Command {
472 inner: self.inner,
473 stdin_data: self.stdin_data,
474 }
475 }
476
477 pub async fn run(mut self) -> Result<CaptureSingleResult, CommandError> {
479 self.inner.stdout(std::process::Stdio::piped());
480
481 let command = Command {
482 inner: self.inner,
483 stdin_data: self.stdin_data,
484 };
485
486 let output = run_capture(command, self.accept_nonzero_exit).await?;
487
488 Ok(CaptureSingleResult {
489 bytes: output.stdout,
490 status: output.status,
491 })
492 }
493
494 pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
496 Ok(self.run().await?.bytes)
497 }
498
499 pub async fn string(self) -> Result<String, CommandError> {
501 let bytes = self.bytes().await?;
502 String::from_utf8(bytes).map_err(|utf8_error| {
503 CommandError::Io(std::io::Error::new(
504 std::io::ErrorKind::InvalidData,
505 utf8_error,
506 ))
507 })
508 }
509}
510
511impl CaptureSingle<Stderr> {
512 #[must_use]
514 pub fn stdout_capture(mut self) -> CaptureAll {
515 self.inner.stdout(std::process::Stdio::piped());
516 self.inner.stderr(std::process::Stdio::piped());
517 CaptureAll {
518 inner: self.inner,
519 stdin_data: self.stdin_data,
520 accept_nonzero_exit: self.accept_nonzero_exit,
521 }
522 }
523
524 #[must_use]
526 pub fn stdout_null(mut self) -> Self {
527 self.inner.stdout(std::process::Stdio::null());
528 self
529 }
530
531 #[must_use]
533 pub fn stdout_inherit(mut self) -> Self {
534 self.inner.stdout(std::process::Stdio::inherit());
535 self
536 }
537
538 #[must_use]
540 pub fn stderr_null(mut self) -> Command {
541 self.inner.stderr(std::process::Stdio::null());
542 Command {
543 inner: self.inner,
544 stdin_data: self.stdin_data,
545 }
546 }
547
548 #[must_use]
550 pub fn stderr_inherit(mut self) -> Command {
551 self.inner.stderr(std::process::Stdio::inherit());
552 Command {
553 inner: self.inner,
554 stdin_data: self.stdin_data,
555 }
556 }
557
558 pub async fn run(mut self) -> Result<CaptureSingleResult, CommandError> {
560 self.inner.stderr(std::process::Stdio::piped());
561
562 let command = Command {
563 inner: self.inner,
564 stdin_data: self.stdin_data,
565 };
566
567 let output = run_capture(command, self.accept_nonzero_exit).await?;
568
569 Ok(CaptureSingleResult {
570 bytes: output.stderr,
571 status: output.status,
572 })
573 }
574
575 pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
577 Ok(self.run().await?.bytes)
578 }
579
580 pub async fn string(self) -> Result<String, CommandError> {
582 let bytes = self.bytes().await?;
583 String::from_utf8(bytes).map_err(|utf8_error| {
584 CommandError::Io(std::io::Error::new(
585 std::io::ErrorKind::InvalidData,
586 utf8_error,
587 ))
588 })
589 }
590}
591
592pub struct CaptureAll {
594 inner: tokio::process::Command,
595 stdin_data: Option<Vec<u8>>,
596 accept_nonzero_exit: bool,
597}
598
599impl CaptureAll {
600 #[must_use]
602 pub fn accept_nonzero_exit(mut self) -> Self {
603 self.accept_nonzero_exit = true;
604 self
605 }
606
607 #[must_use]
609 pub fn stdout_null(mut self) -> CaptureSingle<Stderr> {
610 self.inner.stdout(std::process::Stdio::null());
611 CaptureSingle {
612 inner: self.inner,
613 stdin_data: self.stdin_data,
614 accept_nonzero_exit: self.accept_nonzero_exit,
615 _marker: PhantomData,
616 }
617 }
618
619 #[must_use]
621 pub fn stdout_inherit(mut self) -> CaptureSingle<Stderr> {
622 self.inner.stdout(std::process::Stdio::inherit());
623 CaptureSingle {
624 inner: self.inner,
625 stdin_data: self.stdin_data,
626 accept_nonzero_exit: self.accept_nonzero_exit,
627 _marker: PhantomData,
628 }
629 }
630
631 #[must_use]
633 pub fn stderr_null(mut self) -> CaptureSingle<Stdout> {
634 self.inner.stderr(std::process::Stdio::null());
635 CaptureSingle {
636 inner: self.inner,
637 stdin_data: self.stdin_data,
638 accept_nonzero_exit: self.accept_nonzero_exit,
639 _marker: PhantomData,
640 }
641 }
642
643 #[must_use]
645 pub fn stderr_inherit(mut self) -> CaptureSingle<Stdout> {
646 self.inner.stderr(std::process::Stdio::inherit());
647 CaptureSingle {
648 inner: self.inner,
649 stdin_data: self.stdin_data,
650 accept_nonzero_exit: self.accept_nonzero_exit,
651 _marker: PhantomData,
652 }
653 }
654
655 pub async fn run(mut self) -> Result<CaptureAllResult, CommandError> {
657 self.inner.stdout(std::process::Stdio::piped());
658 self.inner.stderr(std::process::Stdio::piped());
659
660 let command = Command {
661 inner: self.inner,
662 stdin_data: self.stdin_data,
663 };
664
665 let output = run_capture(command, self.accept_nonzero_exit).await?;
666
667 Ok(CaptureAllResult {
668 stdout: output.stdout,
669 stderr: output.stderr,
670 status: output.status,
671 })
672 }
673}
674
675pub struct Command {
676 inner: tokio::process::Command,
677 stdin_data: Option<Vec<u8>>,
678}
679
680impl Command {
681 pub fn new(value: impl AsRef<OsStr>) -> Self {
682 Command {
683 inner: tokio::process::Command::new(value),
684 stdin_data: None,
685 }
686 }
687
688 #[cfg(feature = "test-utils")]
697 pub fn test_eq(&self, other: &Self) {
698 assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
699 }
700
701 pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
702 self.inner.arg(value);
703 self
704 }
705
706 pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
707 if let Some(value) = optional {
708 self.inner.arg(value);
709 }
710 self
711 }
712
713 pub fn optional_flag(mut self, condition: bool, flag: impl AsRef<OsStr>) -> Self {
721 if condition {
722 self.inner.arg(flag);
723 }
724 self
725 }
726
727 pub fn option(mut self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
735 self.inner.arg(name);
736 self.inner.arg(value);
737 self
738 }
739
740 pub fn optional_option(
748 mut self,
749 name: impl AsRef<OsStr>,
750 value: Option<impl AsRef<OsStr>>,
751 ) -> Self {
752 if let Some(value) = value {
753 self.inner.arg(name);
754 self.inner.arg(value);
755 }
756 self
757 }
758
759 pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
760 self.inner.args(value);
761 self
762 }
763
764 pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
765 self.inner.current_dir(dir);
766 self
767 }
768
769 pub fn env(mut self, key: &EnvVariableName, val: impl AsRef<OsStr>) -> Self {
770 self.inner.env(key, val);
771 self
772 }
773
774 pub fn envs<I, V>(mut self, vars: I) -> Self
775 where
776 I: IntoIterator<Item = (EnvVariableName, V)>,
777 V: AsRef<OsStr>,
778 {
779 for (key, val) in vars {
780 self.inner.env(key, val);
781 }
782 self
783 }
784
785 #[must_use]
787 pub fn env_remove(mut self, key: &EnvVariableName) -> Self {
788 self.inner.env_remove(key);
789 self
790 }
791
792 #[must_use]
793 pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
794 self.stdin_data = Some(data.into());
795 self
796 }
797
798 #[must_use]
800 pub fn stdout_capture(self) -> CaptureSingle<Stdout> {
801 CaptureSingle {
802 inner: self.inner,
803 stdin_data: self.stdin_data,
804 accept_nonzero_exit: false,
805 _marker: PhantomData,
806 }
807 }
808
809 #[must_use]
811 pub fn stderr_capture(self) -> CaptureSingle<Stderr> {
812 CaptureSingle {
813 inner: self.inner,
814 stdin_data: self.stdin_data,
815 accept_nonzero_exit: false,
816 _marker: PhantomData,
817 }
818 }
819
820 #[must_use]
822 pub fn stdout_null(mut self) -> Self {
823 self.inner.stdout(std::process::Stdio::null());
824 self
825 }
826
827 #[must_use]
829 pub fn stderr_null(mut self) -> Self {
830 self.inner.stderr(std::process::Stdio::null());
831 self
832 }
833
834 #[must_use]
836 pub fn stdout_inherit(mut self) -> Self {
837 self.inner.stdout(std::process::Stdio::inherit());
838 self
839 }
840
841 #[must_use]
843 pub fn stderr_inherit(mut self) -> Self {
844 self.inner.stderr(std::process::Stdio::inherit());
845 self
846 }
847
848 #[must_use]
853 pub fn build(self) -> tokio::process::Command {
854 self.inner
855 }
856
857 pub async fn status(mut self) -> Result<(), CommandError> {
859 use std::process::Stdio;
860
861 log::debug!("{:#?}", self.inner);
862
863 if self.stdin_data.is_some() {
864 self.inner.stdin(Stdio::piped());
865 }
866
867 let start = std::time::Instant::now();
868
869 let child = self.inner.spawn().map_err(CommandError::Io)?;
870
871 let exit_status = run_and_wait_status(child, self.stdin_data, start).await?;
872
873 if exit_status.success() {
874 Ok(())
875 } else {
876 Err(CommandError::ExitStatus(exit_status))
877 }
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884
885 #[tokio::test]
886 async fn test_stdout_bytes_success() {
887 assert_eq!(
888 Command::new("echo")
889 .argument("hello")
890 .stdout_capture()
891 .bytes()
892 .await
893 .unwrap(),
894 b"hello\n"
895 );
896 }
897
898 #[tokio::test]
899 async fn test_stdout_bytes_nonzero_exit() {
900 let error = Command::new("sh")
901 .arguments(["-c", "exit 42"])
902 .stdout_capture()
903 .bytes()
904 .await
905 .unwrap_err();
906 let CommandError::ExitStatus(status) = error else {
907 panic!("expected ExitStatus, got {error:?}");
908 };
909 assert_eq!(status.code(), Some(42));
910 }
911
912 #[tokio::test]
913 async fn test_stdout_bytes_io_error() {
914 let error = Command::new("./nonexistent")
915 .stdout_capture()
916 .bytes()
917 .await
918 .unwrap_err();
919 let CommandError::Io(io_error) = error else {
920 panic!("expected Io, got {error:?}");
921 };
922 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
923 }
924
925 #[tokio::test]
926 async fn test_stdout_string_success() {
927 assert_eq!(
928 Command::new("echo")
929 .argument("hello")
930 .stdout_capture()
931 .string()
932 .await
933 .unwrap(),
934 "hello\n"
935 );
936 }
937
938 #[tokio::test]
939 async fn test_stderr_bytes_success() {
940 assert_eq!(
941 Command::new("sh")
942 .arguments(["-c", "echo error >&2"])
943 .stderr_capture()
944 .bytes()
945 .await
946 .unwrap(),
947 b"error\n"
948 );
949 }
950
951 #[tokio::test]
952 async fn test_stderr_string_success() {
953 assert_eq!(
954 Command::new("sh")
955 .arguments(["-c", "echo error >&2"])
956 .stderr_capture()
957 .string()
958 .await
959 .unwrap(),
960 "error\n"
961 );
962 }
963
964 #[tokio::test]
965 async fn test_status_success() {
966 assert!(Command::new("true").status().await.is_ok());
967 }
968
969 #[tokio::test]
970 async fn test_status_nonzero_exit() {
971 let error = Command::new("sh")
972 .arguments(["-c", "exit 42"])
973 .status()
974 .await
975 .unwrap_err();
976 let CommandError::ExitStatus(status) = error else {
977 panic!("expected ExitStatus, got {error:?}");
978 };
979 assert_eq!(status.code(), Some(42));
980 }
981
982 #[tokio::test]
983 async fn test_status_io_error() {
984 let error = Command::new("./nonexistent").status().await.unwrap_err();
985 let CommandError::Io(io_error) = error else {
986 panic!("expected Io, got {error:?}");
987 };
988 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
989 }
990
991 #[test]
992 fn test_env_variable_name_from_static_or_panic() {
993 const NAME: EnvVariableName = EnvVariableName::from_static_or_panic("PATH");
994 assert_eq!(NAME.as_str(), "PATH");
995 }
996
997 #[test]
998 fn test_env_variable_name_read_not_present() {
999 const NAME: EnvVariableName =
1000 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_ABSENT_VARIABLE");
1001 let error = NAME.read().unwrap_err();
1002 let EnvVariableReadError::NotPresent { name } = error else {
1003 panic!("expected NotPresent, got {error:?}");
1004 };
1005 assert_eq!(name.as_str(), "CMD_PROC_TEST_ABSENT_VARIABLE");
1006 }
1007
1008 #[test]
1009 fn test_env_variable_name_read_present() {
1010 const NAME: EnvVariableName =
1011 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_PRESENT_VARIABLE");
1012 unsafe { std::env::set_var(NAME.as_str(), "present-value") };
1013 assert_eq!(NAME.read().unwrap().as_str(), "present-value");
1014 }
1015
1016 #[test]
1017 fn test_env_variable_name_read_invalid_value() {
1018 const NAME: EnvVariableName =
1019 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_TOO_LONG_VARIABLE");
1020 let value = "a".repeat(ENV_VARIABLE_VALUE_MAX_LEN + 1);
1021 unsafe { std::env::set_var(NAME.as_str(), &value) };
1022 let error = NAME.read().unwrap_err();
1023 let EnvVariableReadError::InvalidValue { name, source } = error else {
1024 panic!("expected InvalidValue, got {error:?}");
1025 };
1026 assert_eq!(name.as_str(), "CMD_PROC_TEST_TOO_LONG_VARIABLE");
1027 let EnvVariableValueError::TooLong { length, max } = source else {
1028 panic!("expected TooLong, got {source:?}");
1029 };
1030 assert_eq!(length, ENV_VARIABLE_VALUE_MAX_LEN + 1);
1031 assert_eq!(max, ENV_VARIABLE_VALUE_MAX_LEN);
1032 }
1033
1034 #[cfg(unix)]
1035 #[test]
1036 fn test_env_variable_name_read_not_unicode() {
1037 use std::os::unix::ffi::OsStrExt;
1038 const NAME: EnvVariableName =
1039 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_NOT_UNICODE_VARIABLE");
1040 let value = std::ffi::OsStr::from_bytes(&[0xff, 0xfe]);
1041 unsafe { std::env::set_var(NAME.as_str(), value) };
1042 let error = NAME.read().unwrap_err();
1043 let EnvVariableReadError::NotUnicode { name } = error else {
1044 panic!("expected NotUnicode, got {error:?}");
1045 };
1046 assert_eq!(name.as_str(), "CMD_PROC_TEST_NOT_UNICODE_VARIABLE");
1047 }
1048
1049 #[test]
1050 fn test_env_variable_name_is_present() {
1051 const NAME: EnvVariableName =
1052 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_PRESENT_FLAG");
1053 assert!(!NAME.is_present());
1054 unsafe { std::env::set_var(NAME.as_str(), "anything") };
1055 assert!(NAME.is_present());
1056 }
1057
1058 #[test]
1059 fn test_env_variable_name_load_from_str_success() {
1060 const NAME: EnvVariableName =
1061 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_PORT_VARIABLE");
1062 unsafe { std::env::set_var(NAME.as_str(), "8080") };
1063 assert_eq!(NAME.load_from_str::<u16>().unwrap(), 8080);
1064 }
1065
1066 #[test]
1067 fn test_env_variable_name_load_from_str_convert_error() {
1068 const NAME: EnvVariableName =
1069 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_BAD_PORT_VARIABLE");
1070 unsafe { std::env::set_var(NAME.as_str(), "not-a-number") };
1071 let error = NAME.load_from_str::<u16>().unwrap_err();
1072 let EnvVariableLoadError::Convert { name, .. } = error else {
1073 panic!("expected Convert, got {error:?}");
1074 };
1075 assert_eq!(name.as_str(), "CMD_PROC_TEST_BAD_PORT_VARIABLE");
1076 }
1077
1078 #[test]
1079 fn test_env_variable_name_load_from_str_read_error() {
1080 const NAME: EnvVariableName =
1081 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_LOAD_ABSENT_VARIABLE");
1082 let error = NAME.load_from_str::<u16>().unwrap_err();
1083 let EnvVariableLoadError::Read(EnvVariableReadError::NotPresent { name }) = error else {
1084 panic!("expected Read(NotPresent), got {error:?}");
1085 };
1086 assert_eq!(name.as_str(), "CMD_PROC_TEST_LOAD_ABSENT_VARIABLE");
1087 }
1088
1089 #[test]
1090 fn test_env_variable_name_load_try_from_success() {
1091 const NAME: EnvVariableName =
1092 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_TRY_FROM_VARIABLE");
1093 unsafe { std::env::set_var(NAME.as_str(), "value") };
1094 let value: EnvVariableValue = NAME.load_try_from().unwrap();
1095 assert_eq!(value.as_str(), "value");
1096 }
1097
1098 #[derive(Debug)]
1099 struct OnlyFoo;
1100
1101 #[derive(Debug, thiserror::Error)]
1102 #[error("value was not foo")]
1103 struct NotFoo;
1104
1105 impl TryFrom<String> for OnlyFoo {
1106 type Error = NotFoo;
1107
1108 fn try_from(value: String) -> Result<Self, Self::Error> {
1109 if value == "foo" {
1110 Ok(Self)
1111 } else {
1112 Err(NotFoo)
1113 }
1114 }
1115 }
1116
1117 #[test]
1118 fn test_env_variable_name_load_try_from_convert_error() {
1119 const NAME: EnvVariableName =
1120 EnvVariableName::from_static_or_panic("CMD_PROC_TEST_TRY_FROM_BAD_VARIABLE");
1121 unsafe { std::env::set_var(NAME.as_str(), "bar") };
1122 let error = NAME.load_try_from::<OnlyFoo>().unwrap_err();
1123 let EnvVariableLoadError::Convert { name, .. } = error else {
1124 panic!("expected Convert, got {error:?}");
1125 };
1126 assert_eq!(name.as_str(), "CMD_PROC_TEST_TRY_FROM_BAD_VARIABLE");
1127 }
1128
1129 #[test]
1130 fn test_env_variable_name_parse() {
1131 let name: EnvVariableName = "HOME".parse().unwrap();
1132 assert_eq!(name.as_str(), "HOME");
1133 }
1134
1135 #[test]
1136 fn test_env_variable_name_empty() {
1137 let result: Result<EnvVariableName, _> = "".parse();
1138 assert!(matches!(result, Err(EnvVariableNameError::Empty)));
1139 }
1140
1141 #[test]
1142 fn test_env_variable_name_contains_equals() {
1143 let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
1144 assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
1145 }
1146
1147 #[tokio::test]
1148 async fn test_env_with_variable() {
1149 let name: EnvVariableName = "MY_VAR".parse().unwrap();
1150 let output = Command::new("sh")
1151 .arguments(["-c", "echo $MY_VAR"])
1152 .env(&name, "hello")
1153 .stdout_capture()
1154 .string()
1155 .await
1156 .unwrap();
1157 assert_eq!(output, "hello\n");
1158 }
1159
1160 #[tokio::test]
1161 async fn test_stdin_bytes() {
1162 let output = Command::new("cat")
1163 .stdin_bytes(b"hello world".as_slice())
1164 .stdout_capture()
1165 .string()
1166 .await
1167 .unwrap();
1168 assert_eq!(output, "hello world");
1169 }
1170
1171 #[tokio::test]
1172 async fn test_stdin_bytes_vec() {
1173 let output = Command::new("cat")
1174 .stdin_bytes(vec![104, 105])
1175 .stdout_capture()
1176 .string()
1177 .await
1178 .unwrap();
1179 assert_eq!(output, "hi");
1180 }
1181
1182 #[tokio::test]
1183 async fn test_capture_all_success() {
1184 let result = Command::new("echo")
1185 .argument("hello")
1186 .stdout_capture()
1187 .stderr_capture()
1188 .run()
1189 .await
1190 .unwrap();
1191 assert!(result.status.success());
1192 assert_eq!(result.stdout, b"hello\n");
1193 assert!(result.stderr.is_empty());
1194 }
1195
1196 #[tokio::test]
1197 async fn test_capture_all_failure_with_stderr() {
1198 let result = Command::new("sh")
1199 .arguments(["-c", "echo error >&2; exit 1"])
1200 .stdout_capture()
1201 .stderr_capture()
1202 .accept_nonzero_exit()
1203 .run()
1204 .await
1205 .unwrap();
1206 assert!(!result.status.success());
1207 assert_eq!(String::from_utf8(result.stderr).unwrap(), "error\n");
1208 }
1209
1210 #[tokio::test]
1211 async fn test_capture_all_io_error() {
1212 let error = Command::new("./nonexistent")
1213 .stdout_capture()
1214 .stderr_capture()
1215 .run()
1216 .await
1217 .unwrap_err();
1218 let CommandError::Io(io_error) = error else {
1219 panic!("expected Io, got {error:?}");
1220 };
1221 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
1222 }
1223
1224 #[tokio::test]
1225 async fn test_build() {
1226 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1227
1228 let mut child = Command::new("cat")
1229 .build()
1230 .stdin(std::process::Stdio::piped())
1231 .stdout(std::process::Stdio::piped())
1232 .spawn()
1233 .unwrap();
1234
1235 child
1236 .stdin
1237 .as_mut()
1238 .unwrap()
1239 .write_all(b"hello")
1240 .await
1241 .unwrap();
1242 drop(child.stdin.take());
1243
1244 let mut output = String::new();
1245 child
1246 .stdout
1247 .as_mut()
1248 .unwrap()
1249 .read_to_string(&mut output)
1250 .await
1251 .unwrap();
1252 assert_eq!(output, "hello");
1253
1254 let status = child.wait().await.unwrap();
1255 assert!(status.success());
1256 }
1257
1258 #[tokio::test]
1259 async fn test_option() {
1260 let output = Command::new("echo")
1261 .option("-n", "hello")
1262 .stdout_capture()
1263 .string()
1264 .await
1265 .unwrap();
1266 assert_eq!(output, "hello");
1267 }
1268
1269 #[tokio::test]
1270 async fn test_optional_option_some() {
1271 let output = Command::new("echo")
1272 .optional_option("-n", Some("hello"))
1273 .stdout_capture()
1274 .string()
1275 .await
1276 .unwrap();
1277 assert_eq!(output, "hello");
1278 }
1279
1280 #[tokio::test]
1281 async fn test_optional_option_none() {
1282 let output = Command::new("echo")
1283 .optional_option("-n", None::<&str>)
1284 .argument("hello")
1285 .stdout_capture()
1286 .string()
1287 .await
1288 .unwrap();
1289 assert_eq!(output, "hello\n");
1290 }
1291
1292 #[tokio::test]
1293 async fn test_optional_flag_true() {
1294 let output = Command::new("echo")
1295 .optional_flag(true, "-n")
1296 .argument("hello")
1297 .stdout_capture()
1298 .string()
1299 .await
1300 .unwrap();
1301 assert_eq!(output, "hello");
1302 }
1303
1304 #[tokio::test]
1305 async fn test_optional_flag_false() {
1306 let output = Command::new("echo")
1307 .optional_flag(false, "-n")
1308 .argument("hello")
1309 .stdout_capture()
1310 .string()
1311 .await
1312 .unwrap();
1313 assert_eq!(output, "hello\n");
1314 }
1315
1316 #[tokio::test]
1317 async fn test_stdout_null() {
1318 Command::new("echo")
1320 .argument("hello")
1321 .stdout_null()
1322 .status()
1323 .await
1324 .unwrap();
1325 }
1326
1327 #[tokio::test]
1328 async fn test_stderr_null() {
1329 Command::new("sh")
1331 .arguments(["-c", "echo error >&2"])
1332 .stderr_null()
1333 .status()
1334 .await
1335 .unwrap();
1336 }
1337
1338 #[tokio::test]
1339 async fn test_stdout_capture_stderr_null() {
1340 let output = Command::new("sh")
1341 .arguments(["-c", "echo out; echo err >&2"])
1342 .stdout_capture()
1343 .stderr_null()
1344 .string()
1345 .await
1346 .unwrap();
1347 assert_eq!(output, "out\n");
1348 }
1349
1350 #[tokio::test]
1351 async fn test_accept_nonzero_exit_stdout() {
1352 let result = Command::new("sh")
1353 .arguments(["-c", "echo out; exit 42"])
1354 .stdout_capture()
1355 .accept_nonzero_exit()
1356 .run()
1357 .await
1358 .unwrap();
1359 assert!(!result.status.success());
1360 assert_eq!(result.bytes, b"out\n");
1361 }
1362
1363 #[tokio::test]
1364 async fn test_accept_nonzero_exit_capture_all() {
1365 let result = Command::new("sh")
1366 .arguments(["-c", "echo out; echo err >&2; exit 42"])
1367 .stdout_capture()
1368 .stderr_capture()
1369 .accept_nonzero_exit()
1370 .run()
1371 .await
1372 .unwrap();
1373 assert!(!result.status.success());
1374 assert_eq!(result.stdout, b"out\n");
1375 assert_eq!(result.stderr, b"err\n");
1376 }
1377}