cradle/
lib.rs

1#![deny(missing_debug_implementations)]
2
3//! `cradle` provides the [`run!`] macro, that makes
4//! it easy to run child processes from rust programs.
5//!
6//! ```
7//! # let temp_dir = tempfile::TempDir::new().unwrap();
8//! # std::env::set_current_dir(&temp_dir).unwrap();
9//! use cradle::prelude::*;
10//! use std::path::Path;
11//!
12//! run!(%"touch foo");
13//! assert!(Path::new("foo").is_file());
14//! ```
15//!
16//! # Input
17//!
18//! You can pass in multiple arguments (of different types) to [`run!`]
19//! to specify arguments, as long as they implement the [`Input`] trait:
20//!
21//! ```
22//! # let temp_dir = tempfile::TempDir::new().unwrap();
23//! # std::env::set_current_dir(&temp_dir).unwrap();
24//! use cradle::prelude::*;
25//! use std::path::Path;
26//!
27//! run!("mkdir", "-p", "foo/bar/baz");
28//! assert!(Path::new("foo/bar/baz").is_dir());
29//! ```
30//!
31//! For all possible inputs to [`run!`], see the documentation of [`Input`].
32//!
33//! # Output
34//!
35//! `cradle` also provides a [`run_output!`] macro.
36//! It allows to capture outputs of the child process.
37//! It uses return-type polymorphism, so you can control which outputs
38//! are captured by choosing the return type of [`run_output!`].
39//! The only constraint is that the chosen return type has to implement [`Output`].
40//! For example you can use e.g. [`StdoutTrimmed`]
41//! to collect what the child process writes to `stdout`,
42//! trimmed of leading and trailing whitespace:
43//!
44//! ```
45//! use cradle::prelude::*;
46//!
47//! let StdoutTrimmed(output) = run_output!(%"echo foo");
48//! assert_eq!(output, "foo");
49//! ```
50//!
51//! (By default, the child's `stdout` is written to the parent's `stdout`.
52//! Using `StdoutTrimmed` as the return type suppresses that.)
53//!
54//! If you don't want any result from [`run_output!`], you can use `()`
55//! as the return value:
56//!
57//! ```
58//! # let temp_dir = tempfile::TempDir::new().unwrap();
59//! # std::env::set_current_dir(&temp_dir).unwrap();
60//! use cradle::prelude::*;
61//!
62//! let () = run_output!(%"touch foo");
63//! ```
64//!
65//! Since that's a very common case, `cradle` provides the [`run!`] shortcut,
66//! that we've already seen above.
67//! It behaves exactly like [`run_output!`] but always returns `()`:
68//!
69//! ```
70//! # let temp_dir = tempfile::TempDir::new().unwrap();
71//! # std::env::set_current_dir(&temp_dir).unwrap();
72//! use cradle::prelude::*;
73//!
74//! run!(%"touch foo");
75//! ```
76//!
77//! See the implementations for [`output::Output`] for all the supported types.
78//!
79//! # Whitespace Splitting of Inputs
80//!
81//! `cradle` does *not* split given string arguments on whitespace by default.
82//! So for example this code fails:
83//!
84//! ``` should_panic
85//! use cradle::prelude::*;
86//!
87//! let StdoutTrimmed(_) = run_output!("echo foo");
88//! ```
89//!
90//! In this code `cradle` tries to run a process from an executable called
91//! `"echo foo"`, including the space in the file name of the executable.
92//! That fails, because an executable with that name doesn't exist.
93//! `cradle` provides a new-type wrapper [`Split`] to help with that:
94//!
95//! ```
96//! use cradle::prelude::*;
97//!
98//! let StdoutTrimmed(output) = run_output!(Split("echo foo"));
99//! assert_eq!(output, "foo");
100//! ```
101//!
102//! Wrapping an argument of type `&str` in [`Split`] will cause `cradle` to first
103//! split it by whitespace and then use the resulting words as if they were passed
104//! into [`run_output!`] as separate arguments.
105//!
106//! And -- since this is such a common case -- `cradle` provides a syntactic shortcut
107//! for [`Split`], the `%` symbol:
108//!
109//! ```
110//! use cradle::prelude::*;
111//!
112//! let StdoutTrimmed(output) = run_output!(%"echo foo");
113//! assert_eq!(output, "foo");
114//! ```
115//!
116//! # Error Handling
117//!
118//! **tl;dr:** [`run!`] and [`run_output!`] will panic on errors,
119//! [`run_result!`] will not.
120//!
121//! ## Panicking
122//!
123//! By default [`run!`] and [`run_output!`] panic when something goes wrong,
124//! for example when the executable cannot be found or
125//! when a child process exits with a non-zero exit code.
126//! This is by design to allow `cradle` to be used in contexts
127//! where more complex error handling is not needed or desired,
128//! for example in scripts.
129//!
130//! ``` should_panic
131//! use cradle::prelude::*;
132//!
133//! // panics with "false:\n  exited with exit code: 1"
134//! run!("false");
135//! ```
136//!
137//! For a full list of reasons why [`run!`] and [`run_output!`] may panic,
138//! see the documentation of `cradle`'s [`Error`] type.
139//!
140//! ## Preventing Panics
141//!
142//! You can also turn **all** panics into [`std::result::Result::Err`]s
143//! by using [`run_result!`]. This will return a value of type
144//! [`Result<T, cradle::Error>`], where
145//! `T` is any type that implements [`output::Output`].
146//! Here's some examples:
147//!
148//! ```
149//! use cradle::prelude::*;
150//!
151//! let result: Result<(), cradle::Error> = run_result!("false");
152//! let error_message = format!("{}", result.unwrap_err());
153//! assert_eq!(
154//!     error_message,
155//!     "false:\n  exited with exit code: 1"
156//! );
157//!
158//! let result = run_result!(%"echo foo");
159//! let StdoutTrimmed(output) = result.unwrap();
160//! assert_eq!(output, "foo".to_string());
161//! ```
162//!
163//! [`run_result!`] can also be combined with `?` to handle errors in an
164//! idiomatic way, for example:
165//!
166//! ```
167//! use cradle::prelude::*;
168//!
169//! fn build() -> Result<(), Error> {
170//!     run_result!(%"which make")?;
171//!     run_result!(%"which gcc")?;
172//!     run_result!(%"which ld")?;
173//!     run_result!(%"make build")?;
174//!     Ok(())
175//! }
176//! ```
177//!
178//! If you don't want to prevent **all** panics,
179//! but just panics caused by non-zero exit codes,
180//! you can use [`Status`].
181//!
182//! # Alternative Interface: Methods on [`input::Input`]
183//!
184//! `cradle` also provides an alternative interface to execute commands
185//! through methods on the [`Input`] trait:
186//! [`.run()`](Input::run), [`.run_output()`](Input::run_output)
187//! and [`.run_result()`](Input::run_result).
188//! These methods can be invoked on all values whose types implement
189//! [`Input`].
190//! When using these methods, it's especially useful that
191//! [`Input`] is implemented by tuples.
192//! They work analog to [`run!`], [`run_output!`] and [`run_result!`].
193//! Here are some examples:
194//!
195//! ```
196//! # let temp_dir = tempfile::TempDir::new().unwrap();
197//! # std::env::set_current_dir(&temp_dir).unwrap();
198//! use cradle::prelude::*;
199//!
200//! ("touch", "foo").run();
201//!
202//! let StdoutTrimmed(output) = ("echo", "foo").run_output();
203//! assert_eq!(output, "foo");
204//!
205//! let result: Result<(), cradle::Error> = "false".run_result();
206//! let error_message = format!("{}", result.unwrap_err());
207//! assert_eq!(
208//!     error_message,
209//!     "false:\n  exited with exit code: 1"
210//! );
211//! ```
212//!
213//! Note: The `%` shortcut for [`Split`] is not available in this notation.
214//! You can either use tuples, or [`Split`] explicitly:
215//!
216//! ```
217//! use cradle::prelude::*;
218//!
219//! ("echo", "foo").run();
220//! Split("echo foo").run();
221//! ```
222//!
223//! # Prior Art
224//!
225//! `cradle` is heavily inspired by [shake](https://shakebuild.com/),
226//! specifically by its
227//! [`cmd`](https://hackage.haskell.org/package/shake-0.19.4/docs/Development-Shake.html#v:cmd)
228//! function.
229
230pub mod child_output;
231mod collected_output;
232pub mod config;
233mod context;
234pub mod error;
235pub mod input;
236mod macros;
237pub mod output;
238pub mod prelude;
239
240include!("common_re_exports.rs.snippet");
241
242#[cfg(test)]
243mod tests {
244    use crate::{
245        context::Context,
246        input::{run_result_with_context, run_result_with_context_unit},
247        prelude::*,
248    };
249    use lazy_static::lazy_static;
250    use std::{
251        collections::BTreeSet,
252        env::{current_dir, set_current_dir},
253        ffi::{OsStr, OsString},
254        fs,
255        io::Write,
256        path::PathBuf,
257        sync::{Arc, Mutex},
258    };
259    use tempfile::TempDir;
260
261    fn in_temporary_directory<F>(f: F)
262    where
263        F: FnOnce() + std::panic::UnwindSafe,
264    {
265        lazy_static! {
266            static ref CURRENT_DIR_LOCK: Mutex<()> = Mutex::new(());
267        }
268        let _lock = CURRENT_DIR_LOCK.lock();
269        let temp_dir = TempDir::new().unwrap();
270        let original_working_directory = current_dir().unwrap();
271        set_current_dir(&temp_dir).unwrap();
272        let result = std::panic::catch_unwind(|| {
273            f();
274        });
275        set_current_dir(original_working_directory).unwrap();
276        result.unwrap();
277    }
278
279    fn test_executable(name: &str) -> PathBuf {
280        lazy_static! {
281            static ref BUILT: Arc<Mutex<BTreeSet<String>>> = Arc::new(Mutex::new(BTreeSet::new()));
282        }
283        let mut set = match BUILT.lock() {
284            Ok(set) => set,
285            Err(error) => {
286                let _ = write!(
287                    std::io::stderr(),
288                    "test_executable: BUILT poisoned: {}",
289                    error
290                );
291                let _ = std::io::stderr().flush();
292                std::process::exit(1)
293            }
294        };
295        if !set.contains(name) {
296            set.insert(name.to_owned());
297            run!(
298                LogCommand,
299                CurrentDir(std::env::var("CARGO_MANIFEST_DIR").unwrap()),
300                %"cargo build",
301                ("--bin", name),
302                %"--features test_executables",
303            );
304        }
305        executable_path::executable_path(name)
306    }
307
308    fn test_helper() -> PathBuf {
309        test_executable("test_executables_helper")
310    }
311
312    #[test]
313    fn allows_to_execute_a_command() {
314        in_temporary_directory(|| {
315            run!(%"touch foo");
316            assert!(PathBuf::from("foo").exists());
317        })
318    }
319
320    mod errors {
321        use super::*;
322
323        mod panics_by_default {
324            use super::*;
325
326            #[test]
327            #[should_panic(expected = "cradle error: false:\n  exited with exit code: 1")]
328            fn non_zero_exit_codes() {
329                run!("false");
330            }
331
332            #[test]
333            #[should_panic(expected = "cradle error: false:\n  exited with exit code: 1")]
334            fn combine_panics_with_other_outputs() {
335                let StdoutTrimmed(_) = run_output!("false");
336            }
337
338            #[test]
339            #[should_panic(expected = "cradle error: false foo bar:\n  exited with exit code: 1")]
340            fn includes_full_command_on_non_zero_exit_codes() {
341                run!(%"false foo bar");
342            }
343
344            #[test]
345            #[should_panic(expected = "exited with exit code: 42")]
346            fn other_exit_codes() {
347                run!(test_helper(), "exit code 42");
348            }
349
350            #[test]
351            #[should_panic(
352                expected = "cradle error: File not found error when executing 'does-not-exist'"
353            )]
354            fn executable_cannot_be_found() {
355                run!("does-not-exist");
356            }
357
358            #[test]
359            #[cfg(unix)]
360            #[should_panic(expected = "/file foo bar:\n  Permission denied (os error 13)")]
361            fn includes_full_command_on_io_errors() {
362                let temp_dir = TempDir::new().unwrap();
363                let without_executable_bit = temp_dir.path().join("file");
364                fs::write(&without_executable_bit, "").unwrap();
365                run!(without_executable_bit, %"foo bar");
366            }
367
368            #[rustversion::since(1.46)]
369            #[test]
370            fn includes_source_location_of_run_run_call() {
371                let (Status(_), Stderr(stderr)) =
372                    run_output!(test_executable("test_executables_panic"));
373                let expected = "src/test_executables/panic.rs:4:5";
374                assert!(
375                    stderr.contains(expected),
376                    "{:?}\n  does not contain\n{:?}",
377                    stderr,
378                    expected
379                );
380            }
381
382            #[test]
383            #[should_panic(expected = "cradle error: no arguments given")]
384            fn no_executable() {
385                let vector: Vec<String> = Vec::new();
386                run!(vector);
387            }
388
389            #[test]
390            #[should_panic(expected = "invalid utf-8 written to stdout")]
391            fn invalid_utf8_stdout() {
392                let StdoutTrimmed(_) = run_output!(test_helper(), "invalid utf-8 stdout");
393            }
394
395            #[test]
396            #[cfg(not(windows))]
397            fn invalid_utf8_to_stdout_is_allowed_when_not_captured() {
398                run!(test_helper(), "invalid utf-8 stdout");
399            }
400        }
401
402        mod result_types {
403            use super::*;
404            use pretty_assertions::assert_eq;
405
406            #[test]
407            fn non_zero_exit_codes() {
408                let result: Result<(), Error> = run_result!("false");
409                assert_eq!(
410                    result.unwrap_err().to_string(),
411                    "false:\n  exited with exit code: 1"
412                );
413            }
414
415            #[test]
416            fn no_errors() {
417                let result: Result<(), Error> = run_result!("true");
418                result.unwrap();
419            }
420
421            #[test]
422            fn combine_ok_with_other_outputs() {
423                let StdoutTrimmed(output) = run_result!(%"echo foo").unwrap();
424                assert_eq!(output, "foo".to_string());
425            }
426
427            #[test]
428            fn combine_err_with_other_outputs() {
429                let result: Result<StdoutTrimmed, Error> = run_result!("false");
430                assert_eq!(
431                    result.unwrap_err().to_string(),
432                    "false:\n  exited with exit code: 1"
433                );
434            }
435
436            #[test]
437            fn includes_full_command_on_non_zero_exit_codes() {
438                let result: Result<(), Error> = run_result!(%"false foo bar");
439                assert_eq!(
440                    result.unwrap_err().to_string(),
441                    "false foo bar:\n  exited with exit code: 1"
442                );
443            }
444
445            #[test]
446            fn includes_full_command_on_io_errors() {
447                in_temporary_directory(|| {
448                    fs::write("without-executable-bit", "").unwrap();
449                    let result: Result<(), Error> =
450                        run_result!(%"./without-executable-bit foo bar");
451                    assert_eq!(
452                        result.unwrap_err().to_string(),
453                        if cfg!(windows) {
454                            "./without-executable-bit foo bar:\n  %1 is not a valid Win32 application. (os error 193)"
455                        } else {
456                            "./without-executable-bit foo bar:\n  Permission denied (os error 13)"
457                        }
458                    );
459                });
460            }
461
462            #[test]
463            fn other_exit_codes() {
464                let result: Result<(), Error> = run_result!(test_helper(), "exit code 42");
465                assert!(result
466                    .unwrap_err()
467                    .to_string()
468                    .contains("exited with exit code: 42"));
469            }
470
471            #[test]
472            fn missing_executable_file_error_message() {
473                let result: Result<(), Error> = run_result!("does-not-exist");
474                assert_eq!(
475                    result.unwrap_err().to_string(),
476                    "File not found error when executing 'does-not-exist'"
477                );
478            }
479
480            #[test]
481            fn missing_executable_file_error_can_be_matched_against() {
482                let result: Result<(), Error> = run_result!("does-not-exist");
483                match result {
484                    Err(Error::FileNotFound { executable, .. }) => {
485                        assert_eq!(executable, "does-not-exist");
486                    }
487                    _ => panic!("should match Error::FileNotFound"),
488                }
489            }
490
491            #[test]
492            fn missing_executable_file_error_can_be_caused_by_relative_paths() {
493                let result: Result<(), Error> = run_result!("./does-not-exist");
494                match result {
495                    Err(Error::FileNotFound { executable, .. }) => {
496                        assert_eq!(executable, "./does-not-exist");
497                    }
498                    _ => panic!("should match Error::FileNotFound"),
499                }
500            }
501
502            #[test]
503            fn no_executable() {
504                let vector: Vec<String> = Vec::new();
505                let result: Result<(), Error> = run_result!(vector);
506                assert_eq!(result.unwrap_err().to_string(), "no arguments given");
507            }
508
509            #[test]
510            fn invalid_utf8_stdout() {
511                let test_helper = test_helper();
512                let result: Result<StdoutTrimmed, Error> =
513                    run_result!(&test_helper, "invalid utf-8 stdout");
514                assert_eq!(
515                    result.unwrap_err().to_string(),
516                    format!(
517                        "{} 'invalid utf-8 stdout':\n  invalid utf-8 written to stdout",
518                        test_helper.display()
519                    )
520                );
521            }
522        }
523
524        mod whitespace_in_executable_note {
525            use super::*;
526            use pretty_assertions::assert_eq;
527            use unindent::Unindent;
528
529            #[test]
530            fn missing_executable_file_with_whitespace_includes_note() {
531                let result: Result<(), Error> = run_result!("does not exist");
532                let expected = "
533                    File not found error when executing 'does not exist'
534                    note: Given executable name 'does not exist' contains whitespace.
535                      Did you mean to run 'does', with 'not' and 'exist' as arguments?
536                      Consider using Split: https://docs.rs/cradle/latest/cradle/input/struct.Split.html
537                "
538                .unindent()
539                .trim()
540                .to_string();
541                assert_eq!(result.unwrap_err().to_string(), expected);
542            }
543
544            #[test]
545            fn single_argument() {
546                let result: Result<(), Error> = run_result!("foo bar");
547                let expected = "
548                    File not found error when executing 'foo bar'
549                    note: Given executable name 'foo bar' contains whitespace.
550                      Did you mean to run 'foo', with 'bar' as the argument?
551                      Consider using Split: https://docs.rs/cradle/latest/cradle/input/struct.Split.html
552                "
553                .unindent()
554                .trim()
555                .to_string();
556                assert_eq!(result.unwrap_err().to_string(), expected);
557            }
558        }
559    }
560
561    #[test]
562    fn allows_to_retrieve_stdout() {
563        let StdoutTrimmed(stdout) = run_output!(%"echo foo");
564        assert_eq!(stdout, "foo");
565    }
566
567    #[test]
568    fn command_and_argument_as_separate_ref_str() {
569        let StdoutTrimmed(stdout) = run_output!("echo", "foo");
570        assert_eq!(stdout, "foo");
571    }
572
573    #[test]
574    fn multiple_arguments_as_ref_str() {
575        let StdoutTrimmed(stdout) = run_output!("echo", "foo", "bar");
576        assert_eq!(stdout, "foo bar");
577    }
578
579    #[test]
580    fn arguments_can_be_given_as_references() {
581        let reference: &LogCommand = &LogCommand;
582        let executable: &String = &"echo".to_string();
583        let argument: &String = &"foo".to_string();
584        let StdoutTrimmed(stdout) = run_output!(reference, executable, argument);
585        assert_eq!(stdout, "foo");
586    }
587
588    mod sequences {
589        use super::*;
590
591        #[test]
592        fn allows_to_pass_in_arguments_as_a_vec_of_ref_str() {
593            let args: Vec<&str> = vec!["foo"];
594            let StdoutTrimmed(stdout) = run_output!("echo", args);
595            assert_eq!(stdout, "foo");
596        }
597
598        #[test]
599        fn vector_of_non_strings() {
600            let context = Context::test();
601            let log_commands: Vec<LogCommand> = vec![LogCommand];
602            let StdoutTrimmed(stdout) =
603                run_result_with_context(context.clone(), (log_commands, Split("echo foo")))
604                    .unwrap();
605            assert_eq!(stdout, "foo");
606            assert_eq!(context.stderr(), "+ echo foo\n");
607        }
608
609        #[rustversion::since(1.51)]
610        #[test]
611        fn arrays_as_arguments() {
612            let args: [&str; 2] = ["echo", "foo"];
613            let StdoutTrimmed(stdout) = run_output!(args);
614            assert_eq!(stdout, "foo");
615        }
616
617        #[rustversion::since(1.51)]
618        #[test]
619        fn arrays_of_non_strings() {
620            let context = Context::test();
621            let log_commands: [LogCommand; 1] = [LogCommand];
622            let StdoutTrimmed(stdout) =
623                run_result_with_context(context.clone(), (log_commands, Split("echo foo")))
624                    .unwrap();
625            assert_eq!(stdout, "foo");
626            assert_eq!(context.stderr(), "+ echo foo\n");
627        }
628
629        #[rustversion::since(1.51)]
630        #[test]
631        fn elements_in_arrays_are_not_split_by_whitespace() {
632            in_temporary_directory(|| {
633                let args: [&str; 1] = ["foo bar"];
634                run!("touch", args);
635                assert!(PathBuf::from("foo bar").exists());
636            });
637        }
638
639        #[rustversion::since(1.51)]
640        #[test]
641        fn array_refs_as_arguments() {
642            let args: &[&str; 2] = &["echo", "foo"];
643            let StdoutTrimmed(stdout) = run_output!(args);
644            assert_eq!(stdout, "foo");
645        }
646
647        #[rustversion::since(1.51)]
648        #[test]
649        fn elements_in_array_refs_are_not_split_by_whitespace() {
650            in_temporary_directory(|| {
651                let args: &[&str; 1] = &["foo bar"];
652                run!("touch", args);
653                assert!(PathBuf::from("foo bar").exists());
654            });
655        }
656
657        #[test]
658        fn slices_as_arguments() {
659            let args: &[&str] = &["echo", "foo"];
660            let StdoutTrimmed(stdout) = run_output!(args);
661            assert_eq!(stdout, "foo");
662        }
663
664        #[test]
665        fn slices_of_non_strings() {
666            let context = Context::test();
667            let log_commands: &[LogCommand] = &[LogCommand];
668            let StdoutTrimmed(stdout) =
669                run_result_with_context(context.clone(), (log_commands, Split("echo foo")))
670                    .unwrap();
671            assert_eq!(stdout, "foo");
672            assert_eq!(context.stderr(), "+ echo foo\n");
673        }
674
675        #[test]
676        fn elements_in_slices_are_not_split_by_whitespace() {
677            in_temporary_directory(|| {
678                let args: &[&str] = &["foo bar"];
679                run!("touch", args);
680                assert!(PathBuf::from("foo bar").exists());
681            });
682        }
683
684        #[test]
685        fn vector_of_vectors() {
686            let StdoutTrimmed(output) = run_output!(vec![vec!["echo"], vec!["foo", "bar"]]);
687            assert_eq!(output, "foo bar");
688        }
689    }
690
691    mod strings {
692        use super::*;
693
694        #[test]
695        fn works_for_string() {
696            let command: String = "true".to_string();
697            run!(command);
698        }
699
700        #[test]
701        fn multiple_strings() {
702            let command: String = "echo".to_string();
703            let argument: String = "foo".to_string();
704            let StdoutTrimmed(output) = run_output!(command, argument);
705            assert_eq!(output, "foo");
706        }
707
708        #[test]
709        fn mix_ref_str_and_string() {
710            let argument: String = "foo".to_string();
711            let StdoutTrimmed(output) = run_output!("echo", argument);
712            assert_eq!(output, "foo");
713        }
714
715        #[test]
716        fn does_not_split_strings_in_vectors() {
717            in_temporary_directory(|| {
718                let argument: Vec<String> = vec!["filename with spaces".to_string()];
719                run!("touch", argument);
720                assert!(PathBuf::from("filename with spaces").exists());
721            });
722        }
723    }
724
725    mod os_strings {
726        use super::*;
727
728        #[test]
729        fn works_for_os_string() {
730            run!(OsString::from("true"));
731        }
732
733        #[test]
734        fn works_for_os_str() {
735            run!(OsStr::new("true"));
736        }
737    }
738
739    mod stdout {
740        use super::*;
741        use std::{thread, time::Duration};
742
743        #[test]
744        fn relays_stdout_by_default() {
745            let context = Context::test();
746            run_result_with_context_unit(context.clone(), Split("echo foo")).unwrap();
747            assert_eq!(context.stdout(), "foo\n");
748        }
749
750        #[test]
751        fn relays_stdout_for_non_zero_exit_codes() {
752            let context = Context::test();
753            let _ = run_result_with_context_unit(
754                context.clone(),
755                (test_helper(), "output foo and exit with 42"),
756            );
757            assert_eq!(context.stdout(), "foo\n");
758        }
759
760        #[test]
761        fn streams_stdout() {
762            in_temporary_directory(|| {
763                let context = Context::test();
764                let context_clone = context.clone();
765                let thread = thread::spawn(|| {
766                    run_result_with_context_unit(
767                        context_clone,
768                        (test_helper(), "stream chunk then wait for file"),
769                    )
770                    .unwrap();
771                });
772                while (context.stdout()) != "foo\n" {
773                    thread::sleep(Duration::from_secs_f32(0.05));
774                }
775                run!(%"touch file");
776                thread.join().unwrap();
777            });
778        }
779
780        #[test]
781        fn does_not_relay_stdout_when_collecting_into_string() {
782            let context = Context::test();
783            let StdoutTrimmed(_) =
784                run_result_with_context(context.clone(), Split("echo foo")).unwrap();
785            assert_eq!(context.stdout(), "");
786        }
787
788        #[test]
789        fn does_not_relay_stdout_when_collecting_into_result_of_string() {
790            let context = Context::test();
791            let _: Result<StdoutTrimmed, Error> =
792                run_result_with_context(context.clone(), Split("echo foo"));
793            assert_eq!(context.stdout(), "");
794        }
795    }
796
797    mod stderr {
798        use super::*;
799        use pretty_assertions::assert_eq;
800        use std::{thread, time::Duration};
801
802        #[test]
803        fn relays_stderr_by_default() {
804            let context = Context::test();
805            run_result_with_context_unit(context.clone(), (test_helper(), "write to stderr"))
806                .unwrap();
807            assert_eq!(context.stderr(), "foo\n");
808        }
809
810        #[test]
811        fn relays_stderr_for_non_zero_exit_codes() {
812            let context = Context::test();
813            let _: Result<(), Error> = run_result_with_context(
814                context.clone(),
815                (test_helper(), "write to stderr and exit with 42"),
816            );
817            assert_eq!(context.stderr(), "foo\n");
818        }
819
820        #[test]
821        fn streams_stderr() {
822            in_temporary_directory(|| {
823                let context = Context::test();
824                let context_clone = context.clone();
825                let thread = thread::spawn(|| {
826                    run_result_with_context_unit(
827                        context_clone,
828                        (test_helper(), "stream chunk to stderr then wait for file"),
829                    )
830                    .unwrap();
831                });
832                loop {
833                    let expected = "foo\n";
834                    let stderr = context.stderr();
835                    if stderr == expected {
836                        break;
837                    }
838                    assert!(
839                        stderr.len() <= expected.len(),
840                        "expected: {}, got: {}",
841                        expected,
842                        stderr
843                    );
844                    thread::sleep(Duration::from_secs_f32(0.05));
845                }
846                run!(%"touch file");
847                thread.join().unwrap();
848            });
849        }
850
851        #[test]
852        fn capture_stderr() {
853            let Stderr(stderr) = run_output!(test_helper(), "write to stderr");
854            assert_eq!(stderr, "foo\n");
855        }
856
857        #[test]
858        fn assumes_stderr_is_utf_8() {
859            let result: Result<Stderr, Error> = run_result!(test_helper(), "invalid utf-8 stderr");
860            assert_eq!(
861                result.unwrap_err().to_string(),
862                format!(
863                    "{} 'invalid utf-8 stderr':\n  invalid utf-8 written to stderr",
864                    test_helper().display(),
865                )
866            );
867        }
868
869        #[test]
870        #[cfg(not(windows))]
871        fn does_allow_invalid_utf_8_to_stderr_when_not_captured() {
872            run!(test_helper(), "invalid utf-8 stderr");
873        }
874
875        #[test]
876        fn does_not_relay_stderr_when_catpuring() {
877            let context = Context::test();
878            let Stderr(_) =
879                run_result_with_context(context.clone(), (test_helper(), "write to stderr"))
880                    .unwrap();
881            assert_eq!(context.stderr(), "");
882        }
883    }
884
885    mod log_commands {
886        use super::*;
887
888        #[test]
889        fn logs_simple_commands() {
890            let context = Context::test();
891            run_result_with_context_unit(context.clone(), (LogCommand, "true")).unwrap();
892            assert_eq!(context.stderr(), "+ true\n");
893        }
894
895        #[test]
896        fn logs_commands_with_arguments() {
897            let context = Context::test();
898            run_result_with_context_unit(context.clone(), (LogCommand, Split("echo foo"))).unwrap();
899            assert_eq!(context.stderr(), "+ echo foo\n");
900        }
901
902        #[test]
903        fn quotes_arguments_with_spaces() {
904            let context = Context::test();
905            run_result_with_context_unit(context.clone(), (LogCommand, "echo", "foo bar")).unwrap();
906            assert_eq!(context.stderr(), "+ echo 'foo bar'\n");
907        }
908
909        #[test]
910        fn quotes_empty_arguments() {
911            let context = Context::test();
912            run_result_with_context_unit(context.clone(), (LogCommand, "echo", "")).unwrap();
913            assert_eq!(context.stderr(), "+ echo ''\n");
914        }
915
916        #[test]
917        #[cfg(unix)]
918        fn arguments_with_invalid_utf8_will_be_logged_with_lossy_conversion() {
919            use std::{ffi::OsStr, os::unix::prelude::OsStrExt, path::Path};
920            let context = Context::test();
921            let argument_with_invalid_utf8: &OsStr =
922                OsStrExt::from_bytes(&[102, 111, 111, 0x80, 98, 97, 114]);
923            let argument_with_invalid_utf8: &Path = argument_with_invalid_utf8.as_ref();
924            run_result_with_context_unit(
925                context.clone(),
926                (LogCommand, "echo", argument_with_invalid_utf8),
927            )
928            .unwrap();
929            assert_eq!(context.stderr(), "+ echo foo�bar\n");
930        }
931    }
932
933    mod exit_status {
934        use super::*;
935
936        #[test]
937        fn zero() {
938            let Status(exit_status) = run_output!("true");
939            assert!(exit_status.success());
940        }
941
942        #[test]
943        fn one() {
944            let Status(exit_status) = run_output!("false");
945            assert!(!exit_status.success());
946        }
947
948        #[test]
949        fn forty_two() {
950            let Status(exit_status) = run_output!(test_helper(), "exit code 42");
951            assert!(!exit_status.success());
952            assert_eq!(exit_status.code(), Some(42));
953        }
954
955        #[test]
956        fn failing_commands_return_oks_when_exit_status_is_captured() {
957            let Status(exit_status) = run_result!("false").unwrap();
958            assert!(!exit_status.success());
959        }
960    }
961
962    mod bool_output {
963        use super::*;
964
965        #[test]
966        fn success_exit_status_is_true() {
967            assert!(run_output!("true"));
968        }
969
970        #[test]
971        fn failure_exit_status_is_false() {
972            assert!(!run_output!("false"));
973        }
974
975        #[test]
976        #[should_panic]
977        fn io_error_panics() {
978            assert!(run_output!("/"));
979        }
980    }
981
982    mod tuple_inputs {
983        use super::*;
984        use pretty_assertions::assert_eq;
985
986        #[test]
987        fn two_tuple() {
988            let StdoutTrimmed(output) = run_output!(("echo", "foo"));
989            assert_eq!(output, "foo");
990        }
991
992        #[test]
993        fn three_tuples() {
994            let StdoutTrimmed(output) = run_output!(("echo", "foo", "bar"));
995            assert_eq!(output, "foo bar");
996        }
997
998        #[test]
999        fn nested_tuples() {
1000            let StdoutTrimmed(output) = run_output!(("echo", ("foo", "bar")));
1001            assert_eq!(output, "foo bar");
1002        }
1003
1004        #[test]
1005        fn unit_input() {
1006            let StdoutTrimmed(output) = run_output!(("echo", ()));
1007            assert_eq!(output, "");
1008        }
1009    }
1010
1011    mod tuple_outputs {
1012        use super::*;
1013
1014        #[test]
1015        fn two_tuple_1() {
1016            let (StdoutTrimmed(output), Status(exit_status)) =
1017                run_output!(test_helper(), "output foo and exit with 42");
1018            assert_eq!(output, "foo");
1019            assert_eq!(exit_status.code(), Some(42));
1020        }
1021
1022        #[test]
1023        fn two_tuple_2() {
1024            let (Status(exit_status), StdoutTrimmed(output)) =
1025                run_output!(test_helper(), "output foo and exit with 42");
1026            assert_eq!(output, "foo");
1027            assert_eq!(exit_status.code(), Some(42));
1028        }
1029
1030        #[test]
1031        fn result_of_tuple() {
1032            let (StdoutTrimmed(output), Status(exit_status)) = run_result!(%"echo foo").unwrap();
1033            assert_eq!(output, "foo");
1034            assert!(exit_status.success());
1035        }
1036
1037        #[test]
1038        fn result_of_tuple_when_erroring() {
1039            let (StdoutTrimmed(output), Status(exit_status)) = run_result!("false").unwrap();
1040            assert_eq!(output, "");
1041            assert_eq!(exit_status.code(), Some(1));
1042        }
1043
1044        #[test]
1045        fn three_tuples() {
1046            let (Stderr(stderr), StdoutTrimmed(stdout), Status(exit_status)) =
1047                run_output!(%"echo foo");
1048            assert_eq!(stderr, "");
1049            assert_eq!(stdout, "foo");
1050            assert_eq!(exit_status.code(), Some(0));
1051        }
1052
1053        #[test]
1054        fn capturing_stdout_on_errors() {
1055            let (StdoutTrimmed(output), Status(exit_status)) =
1056                run_output!(test_helper(), "output foo and exit with 42");
1057            assert!(!exit_status.success());
1058            assert_eq!(output, "foo");
1059        }
1060
1061        #[test]
1062        fn capturing_stderr_on_errors() {
1063            let (Stderr(output), Status(exit_status)) =
1064                run_output!(test_helper(), "write to stderr and exit with 42");
1065            assert!(!exit_status.success());
1066            assert_eq!(output, "foo\n");
1067        }
1068    }
1069
1070    mod current_dir {
1071        use super::*;
1072        use std::path::Path;
1073
1074        #[test]
1075        fn sets_the_working_directory() {
1076            in_temporary_directory(|| {
1077                fs::create_dir("dir").unwrap();
1078                fs::write("dir/file", "foo").unwrap();
1079                fs::write("file", "wrong file").unwrap();
1080                let StdoutUntrimmed(output) = run_output!(%"cat file", CurrentDir("dir"));
1081                assert_eq!(output, "foo");
1082            });
1083        }
1084
1085        #[test]
1086        fn works_for_other_types() {
1087            in_temporary_directory(|| {
1088                fs::create_dir("dir").unwrap();
1089                let dir: String = "dir".to_string();
1090                run!("true", CurrentDir(dir));
1091                let dir: PathBuf = PathBuf::from("dir");
1092                run!("true", CurrentDir(dir));
1093                let dir: &Path = Path::new("dir");
1094                run!("true", CurrentDir(dir));
1095            });
1096        }
1097    }
1098
1099    mod capturing_stdout {
1100        use super::*;
1101
1102        mod trimmed {
1103            use super::*;
1104
1105            #[test]
1106            fn trims_trailing_whitespace() {
1107                let StdoutTrimmed(output) = run_output!(%"echo foo");
1108                assert_eq!(output, "foo");
1109            }
1110
1111            #[test]
1112            fn trims_leading_whitespace() {
1113                let StdoutTrimmed(output) = run_output!(%"echo -n", " foo");
1114                assert_eq!(output, "foo");
1115            }
1116
1117            #[test]
1118            fn does_not_remove_whitespace_within_output() {
1119                let StdoutTrimmed(output) = run_output!(%"echo -n", "foo bar");
1120                assert_eq!(output, "foo bar");
1121            }
1122
1123            #[test]
1124            fn does_not_modify_output_without_whitespace() {
1125                let StdoutTrimmed(output) = run_output!(%"echo -n", "foo");
1126                assert_eq!(output, "foo");
1127            }
1128
1129            #[test]
1130            fn does_not_relay_stdout() {
1131                let context = Context::test();
1132                let StdoutTrimmed(_) =
1133                    run_result_with_context(context.clone(), Split("echo foo")).unwrap();
1134                assert_eq!(context.stdout(), "");
1135            }
1136        }
1137
1138        mod untrimmed {
1139            use super::*;
1140
1141            #[test]
1142            fn does_not_trim_trailing_newline() {
1143                let StdoutUntrimmed(output) = run_output!(%"echo foo");
1144                assert_eq!(output, "foo\n");
1145            }
1146
1147            #[test]
1148            fn does_not_trim_leading_whitespace() {
1149                let StdoutUntrimmed(output) = run_output!(%"echo -n", " foo");
1150                assert_eq!(output, " foo");
1151            }
1152
1153            #[test]
1154            fn does_not_relay_stdout() {
1155                let context = Context::test();
1156                let StdoutUntrimmed(_) =
1157                    run_result_with_context(context.clone(), Split("echo foo")).unwrap();
1158                assert_eq!(context.stdout(), "");
1159            }
1160        }
1161    }
1162
1163    mod split {
1164        use super::*;
1165
1166        #[test]
1167        fn splits_words_by_whitespace() {
1168            let StdoutTrimmed(output) = run_output!(Split("echo foo"));
1169            assert_eq!(output, "foo");
1170        }
1171
1172        #[test]
1173        fn skips_multiple_whitespace_characters() {
1174            let StdoutUntrimmed(output) = run_output!("echo", Split("foo  bar"));
1175            assert_eq!(output, "foo bar\n");
1176        }
1177
1178        #[test]
1179        fn trims_leading_whitespace() {
1180            let StdoutTrimmed(output) = run_output!(Split(" echo foo"));
1181            assert_eq!(output, "foo");
1182        }
1183
1184        #[test]
1185        fn trims_trailing_whitespace() {
1186            let StdoutUntrimmed(output) = run_output!("echo", Split("foo "));
1187            assert_eq!(output, "foo\n");
1188        }
1189
1190        mod percent_sign {
1191            use super::*;
1192
1193            #[test]
1194            fn splits_words() {
1195                let StdoutUntrimmed(output) = run_output!(%"echo foo");
1196                assert_eq!(output, "foo\n");
1197            }
1198
1199            #[test]
1200            fn works_for_later_arguments() {
1201                let StdoutUntrimmed(output) = run_output!("echo", %"foo\tbar");
1202                assert_eq!(output, "foo bar\n");
1203            }
1204
1205            #[test]
1206            fn for_first_of_multiple_arguments() {
1207                let StdoutUntrimmed(output) = run_output!(%"echo foo", "bar");
1208                assert_eq!(output, "foo bar\n");
1209            }
1210
1211            #[test]
1212            fn non_literals() {
1213                let command = "echo foo";
1214                let StdoutUntrimmed(output) = run_output!(%command);
1215                assert_eq!(output, "foo\n");
1216            }
1217
1218            #[test]
1219            fn in_run() {
1220                run!(%"echo foo");
1221            }
1222
1223            #[test]
1224            fn in_run_result() {
1225                let StdoutTrimmed(_) = run_result!(%"echo foo").unwrap();
1226            }
1227        }
1228    }
1229
1230    mod splitting_with_library_functions {
1231        use super::*;
1232
1233        #[test]
1234        fn allow_to_use_split() {
1235            let StdoutTrimmed(output) = run_output!("echo foo".split(' '));
1236            assert_eq!(output, "foo");
1237        }
1238
1239        #[test]
1240        fn split_whitespace() {
1241            let StdoutTrimmed(output) = run_output!("echo foo".split_whitespace());
1242            assert_eq!(output, "foo");
1243        }
1244
1245        #[test]
1246        fn split_ascii_whitespace() {
1247            let StdoutTrimmed(output) = run_output!("echo foo".split_ascii_whitespace());
1248            assert_eq!(output, "foo");
1249        }
1250    }
1251
1252    mod paths {
1253        use super::*;
1254        use pretty_assertions::assert_eq;
1255        use std::path::Path;
1256
1257        fn write_test_script() -> PathBuf {
1258            if cfg!(unix) {
1259                let file = PathBuf::from("./test-script");
1260                let script = "#!/usr/bin/env bash\necho test-output\n";
1261                fs::write(&file, script).unwrap();
1262                run!(%"chmod +x test-script");
1263                file
1264            } else {
1265                let file = PathBuf::from("./test-script.bat");
1266                let script = "@echo test-output\n";
1267                fs::write(&file, script).unwrap();
1268                file
1269            }
1270        }
1271
1272        #[test]
1273        fn ref_path_as_argument() {
1274            in_temporary_directory(|| {
1275                let file: &Path = Path::new("file");
1276                fs::write(file, "test-contents").unwrap();
1277                let StdoutUntrimmed(output) = run_output!("cat", file);
1278                assert_eq!(output, "test-contents");
1279            })
1280        }
1281
1282        #[test]
1283        fn ref_path_as_executable() {
1284            in_temporary_directory(|| {
1285                let file: &Path = &write_test_script();
1286                let StdoutTrimmed(output) = run_output!(file);
1287                assert_eq!(output, "test-output");
1288            })
1289        }
1290
1291        #[test]
1292        fn path_buf_as_argument() {
1293            in_temporary_directory(|| {
1294                let file: PathBuf = PathBuf::from("file");
1295                fs::write(&file, "test-contents").unwrap();
1296                let StdoutUntrimmed(output) = run_output!("cat", file);
1297                assert_eq!(output, "test-contents");
1298            })
1299        }
1300
1301        #[test]
1302        fn path_buf_as_executable() {
1303            in_temporary_directory(|| {
1304                let file: PathBuf = write_test_script();
1305                let StdoutTrimmed(output) = run_output!(file);
1306                assert_eq!(output, "test-output");
1307            })
1308        }
1309    }
1310
1311    mod stdin {
1312        use super::*;
1313
1314        #[test]
1315        fn allows_to_pass_in_strings_as_stdin() {
1316            let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin("foo"));
1317            assert_eq!(output, "oof");
1318        }
1319
1320        #[test]
1321        fn allows_passing_in_u8_slices_as_stdin() {
1322            let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin(&[0, 1, 2]));
1323            assert_eq!(output, "\x02\x01\x00");
1324        }
1325
1326        #[test]
1327        #[cfg(unix)]
1328        fn stdin_is_closed_by_default() {
1329            let StdoutTrimmed(output) = run_output!(test_helper(), "wait until stdin is closed");
1330            assert_eq!(output, "stdin is closed");
1331        }
1332
1333        #[test]
1334        fn writing_too_many_bytes_into_a_non_reading_child_may_error() {
1335            let big_string = String::from_utf8(vec![b'a'; 2_usize.pow(16) + 1]).unwrap();
1336            let result: Result<(), crate::Error> = run_result!("true", Stdin(big_string));
1337            let message = result.unwrap_err().to_string();
1338            assert!(if cfg!(unix) {
1339                message == "true:\n  Broken pipe (os error 32)"
1340            } else {
1341                [
1342                    "true:\n  The pipe is being closed. (os error 232)",
1343                    "true:\n  The pipe has been ended. (os error 109)",
1344                ]
1345                .contains(&message.as_str())
1346            });
1347        }
1348
1349        #[test]
1350        fn multiple_stdin_arguments_are_all_passed_into_the_child_process() {
1351            let StdoutUntrimmed(output) =
1352                run_output!(test_helper(), "reverse", Stdin("foo"), Stdin("bar"));
1353            assert_eq!(output, "raboof");
1354        }
1355
1356        #[test]
1357        fn works_for_owned_strings() {
1358            let argument: String = "foo".to_string();
1359            let StdoutUntrimmed(output) = run_output!(test_helper(), "reverse", Stdin(argument));
1360            assert_eq!(output, "oof");
1361        }
1362    }
1363
1364    mod invocation_syntax {
1365        use super::*;
1366
1367        #[test]
1368        fn trailing_comma_is_accepted_after_normal_argument() {
1369            run!("echo", "foo",);
1370            let StdoutUntrimmed(_) = run_output!("echo", "foo",);
1371            let _result: Result<(), Error> = run_result!("echo", "foo",);
1372        }
1373
1374        #[test]
1375        fn trailing_comma_is_accepted_after_split_argument() {
1376            run!("echo", %"foo",);
1377            let StdoutUntrimmed(_) = run_output!("echo", %"foo",);
1378            let _result: Result<(), Error> = run_result!("echo", %"foo",);
1379        }
1380    }
1381
1382    mod environment_variables {
1383        use super::*;
1384        use pretty_assertions::assert_eq;
1385        use std::env;
1386
1387        #[test]
1388        fn allows_to_add_variables() {
1389            let StdoutTrimmed(output) = run_output!(
1390                test_helper(),
1391                %"echo FOO",
1392                Env("FOO", "bar")
1393            );
1394            assert_eq!(output, "bar");
1395        }
1396
1397        #[test]
1398        fn works_for_multiple_variables() {
1399            let StdoutUntrimmed(output) = run_output!(
1400                test_helper(),
1401                %"echo FOO BAR",
1402                Env("FOO", "a"),
1403                Env("BAR", "b")
1404            );
1405            assert_eq!(output, "a\nb\n");
1406        }
1407
1408        fn find_unused_environment_variable() -> String {
1409            let mut i = 0;
1410            loop {
1411                let key = format!("CRADLE_TEST_VARIABLE_{}", i);
1412                if env::var_os(&key).is_none() {
1413                    break key;
1414                }
1415                i += 1;
1416            }
1417        }
1418
1419        #[test]
1420        fn child_processes_inherit_the_environment() {
1421            let unused_key = find_unused_environment_variable();
1422            env::set_var(&unused_key, "foo");
1423            let StdoutTrimmed(output) = run_output!(test_helper(), "echo", unused_key);
1424            assert_eq!(output, "foo");
1425        }
1426
1427        #[test]
1428        fn overwrites_existing_parent_variables() {
1429            let unused_key = find_unused_environment_variable();
1430            env::set_var(&unused_key, "foo");
1431            let StdoutTrimmed(output) =
1432                run_output!(test_helper(), "echo", &unused_key, Env(&unused_key, "bar"));
1433            assert_eq!(output, "bar");
1434        }
1435
1436        #[test]
1437        fn variables_are_overwritten_by_subsequent_variables_with_the_same_name() {
1438            let StdoutTrimmed(output) = run_output!(
1439                test_helper(),
1440                "echo",
1441                "FOO",
1442                Env("FOO", "a"),
1443                Env("FOO", "b"),
1444            );
1445            assert_eq!(output, "b");
1446        }
1447
1448        #[test]
1449        fn variables_can_be_set_to_the_empty_string() {
1450            let StdoutUntrimmed(output) =
1451                run_output!(test_helper(), "echo", "FOO", Env("FOO", ""),);
1452            assert_eq!(output, "empty variable: FOO\n");
1453        }
1454    }
1455
1456    mod run_interface {
1457        use super::*;
1458        use std::path::Path;
1459
1460        #[test]
1461        fn allows_to_run_commands_with_dot_run() {
1462            let StdoutTrimmed(output) = Split("echo foo").run_output();
1463            assert_eq!(output, "foo");
1464        }
1465
1466        #[test]
1467        fn allows_to_bundle_arguments_up_in_tuples() {
1468            let StdoutTrimmed(output) = ("echo", "foo").run_output();
1469            assert_eq!(output, "foo");
1470        }
1471
1472        #[test]
1473        fn works_for_different_output_types() {
1474            let Status(status) = "false".run_output();
1475            assert!(!status.success());
1476        }
1477
1478        #[test]
1479        fn run() {
1480            in_temporary_directory(|| {
1481                ("touch", "foo").run();
1482                assert!(Path::new("foo").exists());
1483            });
1484        }
1485
1486        #[test]
1487        fn run_result() {
1488            let StdoutTrimmed(output) = ("echo", "foo").run_result().unwrap();
1489            assert_eq!(output, "foo");
1490            let result: Result<(), Error> = "does-not-exist".run_result();
1491            match result {
1492                Err(Error::FileNotFound { .. }) => {}
1493                _ => panic!("should match Error::FileNotFound"),
1494            }
1495        }
1496    }
1497}