cli_sandbox/
lib.rs

1//! <span align="center">
2//!
3//! <h1><pre><code>cli-sandbox</code></pre></h1>
4//!
5//! <a href="https://crates.io/crates/cli-sandbox"><img src="https://img.shields.io/crates/d/cli-sandbox?style=for-the-badge&logo=rust"></img></a>
6//! <a href="https://docs.rs/cli-sandbox"><img src="https://img.shields.io/docsrs/cli-sandbox?style=for-the-badge&logo=docsdotrs"></img></a>
7//!
8//! </span>
9//!
10//! `cli-sandbox` is a sandboxing environment and testing utility to help you test and debug your CLI applications, inspired by [Cargo's `cargo-test-support`](https://github.com/rust-lang/cargo/tree/master/crates/cargo-test-support).
11//!
12//! All tests get their own temporary directories, where you can create files, check files, test your program against those files and check the output of your program in various ways.
13//!
14//! For example, if you want to check that your Python to Rust transpiler works correctly:
15//!
16//! ```rust
17//! use cli_sandbox::{project, WithStdout};
18//! use std::error::Error;
19//!
20//! #[test]
21//! fn compiling() -> Result<(), Box<dyn Error>> {
22//!     cli_sandbox::init(); // Initialize the sandbox
23//!     let proj = project()?;                      // Create a project
24//!
25//!     // Let's create a file, and put in there some Python.
26//!     proj.new_file("my-program.py",
27//! r#"def main():
28//!     print("Hi! this is a test")
29//!
30//! main()"#)?;
31//!
32//!     let cmd = proj.command(["build"])?;         // Execute the command "<YOUR COMMAND> build". Cli-sandbox will automatically get pickup your command.
33//!
34//!     // Now, let's check that the transpiler created the file correctly.
35//!     proj.check_file("my-program.rs",
36//! r#"fn main() {
37//!     println!("Hi! this is a test");
38//! }
39//!
40//! main()"#)?;
41//!
42//!     // And that the command stdout and stderr are correct.
43//!
44//!     cmd.with_stdout("File transpiled correctly! (`my-program.py` -> `my-program.rs`)");
45//!
46//!     // If the stderr isn't empty, we'll panic.
47//!     if !cmd.empty_stderr() {
48//!         panic!("Something went wrong! stderr isn't empty");
49//!     };
50//! }
51//! ```
52//!
53//! You can also get the path of a project (it changes each time the tests are executed, they're temporary).
54//!
55//! ## Installation
56//!
57//! ```sh
58//! cargo add cli-sandbox --dev
59//! ```
60//!
61//! ## Usage
62//!
63//! The first step is to create a `Project`. You can use either `Project::new()` or `project()`. This will create a temporary directory for you to put all your testing files in there.
64//!
65//! From a project, you can execute commands, do I/O operations or even operate over it manually by getting the project's path (`Project::path()`).
66//!
67//! Check the [project's documentation](https://docs.rs/cli-sandbox) for more info.
68//!
69//! ## Features
70//!
71//! * Regex support for checking `stdout` and `stderr`. (feature: `regex`)
72//! * All output is beautiful thanks to [`pretty-assertions`](https://docs.rs/pretty_assertions/latest/pretty_assertions/) and [`better_panic`](https://docs.rs/better_panic). (feature: `pretty`, also can be enabled individually)
73//! * Little fuzzing functionality (feature: `fuzz`)
74//! * Testing either the `debug` or `release` profile (features: `dev` or `release`)
75//!
76
77// All code blocks in fragments must be ignored because rustdoc hates environment variables, it seems.
78
79#![cfg_attr(feature = "deny-warnings", deny(warnings))] // Use for tests
80#![warn(
81    unused,
82    clippy::dbg_macro,
83    clippy::decimal_literal_representation,
84    clippy::undocumented_unsafe_blocks,
85    clippy::empty_structs_with_brackets,
86    clippy::format_push_string,
87    clippy::get_unwrap,
88    clippy::if_then_some_else_none,
89    clippy::impl_trait_in_params,
90    clippy::integer_division,
91    clippy::large_include_file,
92    clippy::let_underscore_must_use,
93    clippy::semicolon_outside_block,
94    clippy::str_to_string,
95    clippy::todo,
96    clippy::unimplemented,
97    clippy::unneeded_field_pattern,
98    clippy::use_debug,
99    clippy::branches_sharing_code,
100    clippy::cast_possible_wrap,
101    clippy::doc_markdown,
102    clippy::empty_enum,
103    clippy::if_not_else,
104    clippy::inefficient_to_string,
105    clippy::items_after_statements,
106    clippy::large_digit_groups,
107    clippy::large_types_passed_by_value,
108    clippy::match_same_arms,
109    clippy::missing_const_for_fn,
110    clippy::missing_panics_doc,
111    clippy::needless_bitwise_bool,
112    clippy::needless_collect,
113    clippy::needless_pass_by_value,
114    clippy::no_effect_underscore_binding,
115    clippy::nonstandard_macro_braces,
116    clippy::or_fun_call,
117    clippy::range_plus_one,
118    clippy::range_minus_one,
119    clippy::similar_names,
120    clippy::suboptimal_flops,
121    clippy::too_many_lines,
122    clippy::unused_self
123)]
124
125use std::{
126    env,
127    ffi::OsStr,
128    fs::{write, File},
129    io::Read,
130    os,
131    path::Path,
132    process::{Command, Output},
133    str,
134};
135
136use anyhow::Result;
137#[cfg(feature = "better_panic")]
138pub use better_panic;
139#[cfg(feature = "pretty_assertions")]
140use pretty_assertions::assert_eq;
141#[cfg(feature = "regex")]
142use regex::Regex;
143use tempfile::{tempdir, TempDir};
144#[cfg(feature = "better_panic")]
145pub mod panic {
146    use better_panic::{Settings, Verbosity};
147
148    /// Shortcut to `better_panic::Settings::new().verbosity(better_panic::Verbosity::Minimal).install()`;
149    ///
150    /// Meant to be used at the start of your tests.
151    #[inline]
152    pub fn minimal() {
153        Settings::new().verbosity(Verbosity::Minimal).install();
154    }
155
156    /// Shortcut to `better_panic::Settings::new().verbosity(better_panic::Verbosity::Medium).install()`;
157    ///
158    /// Meant to be used at the start of your tests.
159    #[inline]
160    pub fn medium() {
161        Settings::new().verbosity(Verbosity::Medium).install();
162    }
163
164    /// Shortcut to `better_panic::Settings::new().verbosity(better_panic::Verbosity::Full).install()`;
165    ///
166    /// Meant to be used at the start of your tests.
167    #[inline]
168    pub fn full() {
169        Settings::new().verbosity(Verbosity::Full).install();
170    }
171}
172
173#[derive(Debug)]
174pub struct Project {
175    tempdir: TempDir,
176}
177
178/// Shortcut for [`Project::new()`].
179#[inline(always)]
180pub fn project() -> Result<Project> {
181    Project::new()
182}
183
184/// Initializes a new sandbox testing environment. Note that **this doesn't initialize a project**, just creates some
185/// environment variables with metadata about your project.
186///
187/// # Panics
188///
189/// This function may panic if it cannot find the root package metadata (a.k.a your project's metadata).
190pub fn init() {
191    let md = cargo_metadata::MetadataCommand::new()
192        .exec()
193        .expect("Couldn't get Cargo Metadata");
194
195    let root = md.root_package().unwrap();
196    env::set_var("SANDBOX_TARGET_DIR", &md.target_directory);
197    env::set_var("SANDBOX_PKG_NAME", &root.name);
198}
199
200impl Project {
201    /// Creates a new [`Project`]
202    ///
203    pub fn new() -> Result<Self> {
204        Ok(Self {
205            tempdir: tempdir()?,
206        })
207    }
208
209    /// Gets the [`std::path::Path`] for the [`Project`]'s temporary directory.
210    pub fn path(&self) -> &Path {
211        self.tempdir.path()
212    }
213
214    /// Creates a new file with a relative path to the project's directory.
215    ///
216    /// `path` gets redirected to the project's real path (temporary and unknown).
217    #[inline]
218    pub fn new_file<P: AsRef<Path>>(&mut self, path: P, contents: &str) -> Result<()> {
219        Ok(write(self.path().join(path), contents)?)
220    }
221
222    /// Checks that the contents of a file are correct. It will panic if they aren't, and show the differences if the feature **`pretty_assertions`** is enabled
223    ///
224    /// `path` gets redirected to the project's real path (temporary and unknown)
225    /// # Panics
226    /// Will panic if the contents of the file at path aren't encoded as UTF-8
227    pub fn check_file<P: AsRef<Path>>(&self, path: P, contents: &str) -> Result<()> {
228        let mut f = File::open(self.path().join(path))?;
229        let mut buf = Vec::new();
230        f.read_to_end(&mut buf)?;
231        let mut buf2 = String::new();
232        buf2.push_str(match str::from_utf8(&buf) {
233            Ok(val) => val,
234            Err(_) => panic!("buf isn't UTF-8 (bug)"),
235        });
236        assert_eq!(buf2, contents);
237        Ok(())
238    }
239
240    /// Executes a command relative to the project's directory
241    pub fn command<I, S>(&self, args: I) -> Result<Output>
242    where
243        I: IntoIterator<Item = S>,
244        S: AsRef<OsStr>,
245    {
246        #[cfg(feature = "dev")]
247        return Ok(Command::new(
248            Path::new(&std::env::var("SANDBOX_TARGET_DIR")?)
249                .join("debug")
250                .join(std::env::var("SANDBOX_PKG_NAME")?),
251        )
252        .current_dir(self.path())
253        .args(args)
254        .output()?);
255
256        #[cfg(feature = "release")]
257        return Ok(Command::new(
258            Path::new(&std::env::var("CARGO_MANIFEST_DIR")?)
259                .join("target")
260                .join("release")
261                .join(env!("CARGO_PKG_NAME")),
262        )
263        .current_dir(&self.path())
264        .args(args)
265        .output()?);
266    }
267
268    /// Checks the [file signature](https://en.m.wikipedia.org/wiki/File_format#Magic_number) of a file and returns `true` if the file in that path is an executable.
269    ///
270    /// The checked file signatures are the following, if the file's signature is any of the following, the function will return `true`.
271    ///
272    /// * `4D 5A`
273    ///     - DOS MZ executable and descendants | (.exe, .scr, .sys, .dll, .fon, .cpl, .iec, .ime, .rs, .tsp, .mz)
274    /// * `5A 4D`
275    ///     - DOS ZM executable and descendants, rare | (.exe)
276    /// * `7F 45 4C 46`
277    ///     - ELF files
278    /// * `64 65 78 0A 30 33 35 00`
279    ///     - [Dalvik Executables](https://en.wikipedia.org/wiki/Dalvik_(software))
280    /// * `4A 6F 79 21`
281    ///     - Preferred Executable Format
282    /// * `00 00 03 F3`
283    ///     - Amiga Hunk executable file
284    pub fn is_bin<P: AsRef<Path>>(&self, path: P) -> bool {
285        let mut buf: [u8; 8] = [0; 8];
286        let mut f = File::open(self.path().join(&path)).expect("Couldn't open that path");
287        match f.read_exact(&mut buf) {
288            Ok(()) => {}
289            Err(_) => {
290                buf.fill(0x01) // Fill the rest of the buffer with 0x01, could have used 0x00 but Dalvik Executable
291                               // already ends with 0x00 and that would make false positives
292            }
293        };
294
295        match buf {
296            [0x4D, 0x5A, ..] | [0x5A, 0x4D, ..] | // DOS MZ (.exe, .scr, .sys, .dll, .fon, .cpl, .iec, .ime, .rs, .tsp, .mz)
297            [0x7F, 0x45, 0x4C, 0x46, ..] | // ELF
298            [0x64, 0x65, 0x78, 0x0A, 0x30, 0x33, 0x35, 0x00] | // Dalvik Executable (.dex)
299            [0x4A, 0x6F, 0x79, 0x21, ..] | // Preferred Executable Format
300            [0x00, 0x00, 0x03, 0xF3, ..] // Amiga Hunk Executable File
301            => {
302                true
303            }
304            _ => {
305                false
306            }
307        }
308    }
309
310    /// Creates a [symbolic link](wikipedia.org/wiki/Symlinks), both paths are relative to the temporary project's path.
311    ///
312    /// # Panics
313    ///
314    /// This function will panic if the OS can't create a system between the two paths.
315    pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, src: P, dst: Q) {
316        let src = self.path().join(src.as_ref());
317        let dst = self.path().join(dst.as_ref());
318        #[cfg(unix)]
319        {
320            if let Err(e) = os::unix::fs::symlink(&src, &dst) {
321                panic!("failed to symlink {:?} to {:?}: {:?}", src, dst, e);
322            }
323        }
324        #[cfg(windows)]
325        {
326            if src.is_dir() {
327                if let Err(e) = os::windows::fs::symlink_dir(&src, &dst) {
328                    panic!("failed to symlink {:?} to {:?}: {:?}", src, dst, e);
329                }
330            } else {
331                if let Err(e) = os::windows::fs::symlink_file(&src, &dst) {
332                    panic!("failed to symlink {:?} to {:?}: {:?}", src, dst, e);
333                }
334            }
335        }
336    }
337
338    /// Cleans your environment used in the working directory (i.e. removing all environment variables that start with a prefix).
339    ///
340    /// ---
341    ///
342    /// (Note that "localized" environment variables are taken into account)
343    ///
344    /// # Panics
345    ///
346    /// This function will panic if it couldn't get the current working directory, or;
347    /// it can't set the current working directory to the temporary project's path, or;
348    /// it can't set the current working directory to the original working directory.
349    pub fn clean_env(&self, prefix: &str) {
350        let cwd = env::current_dir().expect("Couldn't get current working directory");
351        // Some environment variables contain some bash script for being defined in X directory (e.g. PROMPT_COMMAND='[[ $PWD == "/foo/bar/" ]] && export FOO=BAR || unset FOO').
352        // This will define FOO=BAR only if the current working directory is "/foo/bar".
353        // That's why we change the CWD
354        env::set_current_dir(self.path()).expect("Couldn't change path");
355        for (k, _) in env::vars() {
356            if k.starts_with(prefix) {
357                env::remove_var(k);
358            };
359        }
360
361        env::set_current_dir(cwd).expect("Couldn't return to the origin directory");
362    }
363}
364
365pub trait WithStdout {
366    /// Checks that the standard output of a command is what's expected. If they aren't the same, it will show the differences if the `pretty_asssertions` feature is enabled
367    ///
368    /// ## Example
369    /// ```no_run
370    /// # use crate::cli_sandbox::WithStdout;
371    /// # use std::error::Error;
372    /// # use cli_sandbox::project;
373    /// # fn main() -> Result<(), Box<dyn Error>>{
374    /// let proj = project()?;
375    /// let cmd = proj.command(["my", "cool", "--args"])?;
376    /// cmd.with_stdout("Expected stdout");
377    /// # Ok(())
378    /// # }
379    /// ```
380    fn with_stdout<S: AsRef<str>>(&self, stdout: S);
381    /// Checks that the standard error of a command is what's expected. If they aren't the same, it will show the differences if the `pretty_asssertions` feature is enabled
382    ///
383    /// ## Example
384    /// ```no_run
385    /// # use std::error::Error;
386    /// # use cli_sandbox::{project, WithStdout};
387    /// # fn main() -> Result<(), Box<dyn Error>>{
388    /// let proj = project()?;
389    /// let cmd = proj.command(["my", "cool", "--args"])?;
390    /// cmd.with_stderr("Expected stderr");
391    /// # Ok(())
392    /// # }
393    /// ```
394    fn with_stderr<S: AsRef<str>>(&self, stderr: S);
395    /// Checks that the standard output of a command is what's expected (Using regex). If they aren't the same, it will show the differences if the `pretty_asssertions` feature is enabled
396    ///
397    /// ## Example
398    /// ```no_run
399    /// # use std::error::Error;
400    /// # use cli_sandbox::{project, WithStdout};
401    /// # fn main() -> Result<(), Box<dyn Error>>{
402    /// let proj = project()?;
403    /// let cmd = proj.command(["my", "cool", "--args"])?;
404    /// cmd.with_stdout_regex("<Regex that matches expected stdout>");
405    /// # Ok(())
406    /// # }
407    /// ```
408    #[cfg(feature = "regex")]
409    fn with_stdout_regex<S: AsRef<str>>(&self, stdout: S);
410    /// Checks that the standard error of a command is what's expected (Using regex). If they aren't the same, it will show the differences if the `pretty_asssertions` feature is enabled
411    ///
412    /// ## Example
413    /// ```no_run
414    /// # use std::error::Error;
415    /// # use cli_sandbox::{project, WithStdout};
416    /// # fn main() -> Result<(), Box<dyn Error>>{
417    /// let proj = project()?;
418    /// let cmd = proj.command(["my", "cool", "--args"])?;
419    /// cmd.with_stderr("<Regex that matches expected stderr>");
420    /// # Ok(())
421    /// # }
422    /// ```
423    #[cfg(feature = "regex")]
424    fn with_stderr_regex<S: AsRef<str>>(&self, stderr: S);
425    /// Returns how many times the program contains the word "warning:" in the `stderr`. Useful for checking compile-time warnings.
426    ///
427    /// ## Example
428    ///
429    /// ```no_run
430    /// # use std::error::Error;
431    /// # use cli_sandbox::{project, WithStdout};
432    /// # fn main() -> Result<(), Box<dyn Error>> {
433    /// let proj = project()?;
434    /// let cmd = proj.command(["my", "cool", "--args"])?;
435    /// if cmd.stderr_warns() {
436    ///     // Maybe there's something to check with that code...
437    /// }
438    /// # Ok(())
439    /// }
440    /// ```
441    fn stdout_warns(&self) -> bool;
442    /// Returns how many times the program contains the word "warning:" in the `stderr`. Useful for checking compile-time warnings.
443    ///
444    /// ## Example
445    ///
446    /// ```no_run
447    /// # use std::error::Error;
448    /// # use cli_sandbox::{project, WithStdout};
449    /// # fn main() -> Result<(), Box<dyn Error>> {
450    /// let proj = project()?;
451    /// let cmd = proj.command(["my", "cool", "--args"])?;
452    /// if cmd.stderr_warns() {
453    ///     // Maybe there's something to check with that code...
454    /// }
455    /// # Ok(())
456    /// }
457    /// ```
458    fn stderr_warns(&self) -> bool;
459    /// Checks that the stderr is empty. It's different from `.with_stderr("")` in that this won't print a whole diff. Useful for when ANY presence of a stderr would mean that there were errors, and the output is invalid.
460    ///
461    /// ## Example
462    ///
463    /// ```no_run
464    /// # use std::error::Error;
465    /// # use cli_sandbox::{project, WithStdout};
466    /// # fn main() -> Result<(), Box<dyn Error>> {
467    /// let proj = project()?;
468    /// let cmd = proj.command(["my", "cool", "--args"])?;
469    /// if !cmd.empty_stderr() {
470    ///     panic!("HELP!!! THE OUTPUT IS INVALID!!");
471    /// }
472    /// # Ok(())
473    /// }
474    /// ```
475    fn empty_stderr(&self) -> bool;
476    /// Checks that the stdout is empty. It's different from `.with_stdout("")` in that this won't print a whole diff. Useful for when ANY presence of a stdout, would mean that there were errors, and the output is invalid.
477    ///
478    /// ## Example
479    ///
480    /// ```no_run
481    /// # use std::error::Error;
482    /// # use cli_sandbox::{project, WithStdout};
483    /// # fn main() -> Result<(), Box<dyn Error>> {
484    /// let proj = project()?;
485    /// let cmd = proj.command(["my", "cool", "--args"])?;
486    /// if !cmd.empty_stdout() {
487    ///     panic!("HELP!!! THE OUTPUT IS INVALID!!");
488    /// }
489    /// # Ok(())
490    /// }
491    /// ```
492    fn empty_stdout(&self) -> bool;
493    /// Checks that the stdout is corresponding with a file (usually "<my-test>.stdout");
494    ///
495    /// # Example
496    ///
497    /// ```no_run
498    /// # use std::error::Error;
499    /// # use cli_sandbox::{project, WithStdout};
500    /// # fn main() -> Result<(), Box<dyn Error>> {
501    /// let proj = project()?;
502    /// let cmd = proj.command(["my", "cool", "--args"])?;
503    /// cmd.with_stdout_file("cool-args-test.stdout");
504    /// # Ok(())
505    /// # }
506    /// ```
507    fn with_stdout_file<P: AsRef<Path>>(&self, filename: P);
508    /// Checks that the stderr is corresponding with a file (usually "<my-test>.stderr");
509    ///
510    /// # Example
511    ///
512    /// ```no_run
513    /// # use std::error::Error;
514    /// # use cli_sandbox::{project, WithStdout};
515    /// # fn main() -> Result<(), Box<dyn Error>> {
516    /// let proj = project()?;
517    /// let cmd = proj.command(["my", "cool", "--args"])?;
518    /// cmd.with_stderr_file("cool-args-test.stderr");
519    /// # Ok(())
520    /// # }
521    /// ```
522    fn with_stderr_file<P: AsRef<Path>>(&self, filename: P);
523}
524
525impl WithStdout for Output {
526    fn with_stdout<S: AsRef<str>>(&self, stdout: S) {
527        let mut buf = String::new();
528        buf.push_str(match str::from_utf8(&self.stdout) {
529            Ok(val) => val,
530            Err(_) => panic!("stdout isn't UTF-8 (bug)"),
531        });
532        assert_eq!(buf, stdout.as_ref());
533    }
534
535    fn with_stderr<S: AsRef<str>>(&self, stderr: S) {
536        let mut buf = String::new();
537        buf.push_str(match str::from_utf8(&self.stderr) {
538            Ok(val) => val,
539            Err(_) => panic!("stderr isn't UTF-8 (bug)"),
540        });
541        assert_eq!(buf, stderr.as_ref());
542    }
543
544    #[cfg(feature = "regex")]
545    fn with_stderr_regex<S: AsRef<str>>(&self, regex: S) {
546        let re = match Regex::new(regex.as_ref()) {
547            Ok(re) => re,
548            Err(e) => panic!("Regex {} isn't valid: {e}", regex.as_ref()),
549        };
550
551        let mut buf = String::new();
552        buf.push_str(match str::from_utf8(&self.stderr) {
553            Ok(val) => val,
554            Err(_) => panic!("stderr isn't UTF-8 (bug)"),
555        });
556
557        if !re.is_match(&buf) {
558            assert_eq!(buf, regex.as_ref()); // Show differences
559        };
560    }
561
562    #[cfg(feature = "regex")]
563    fn with_stdout_regex<S: AsRef<str>>(&self, regex: S) {
564        let re = match Regex::new(regex.as_ref()) {
565            Ok(re) => re,
566            Err(e) => panic!("Regex {} isn't valid: {e}", regex.as_ref()),
567        };
568
569        let mut buf = String::new();
570        buf.push_str(match str::from_utf8(&self.stdout) {
571            Ok(val) => val,
572            Err(_) => panic!("stdout isn't UTF-8 (bug)"),
573        });
574
575        if !re.is_match(&buf) {
576            assert_eq!(buf, regex.as_ref()); // Show differences
577        };
578    }
579
580    fn stdout_warns(&self) -> bool {
581        let mut buf = String::new();
582        buf.push_str(match str::from_utf8(&self.stdout) {
583            Ok(val) => val,
584            Err(_) => panic!("stdout isn't UTF-8 (bug)"),
585        });
586        buf.contains("warnings:")
587    }
588
589    fn stderr_warns(&self) -> bool {
590        let mut buf = String::new();
591        buf.push_str(match str::from_utf8(&self.stderr) {
592            Ok(val) => val,
593            Err(_) => panic!("stderr isn't UTF-8 (bug)"),
594        });
595        buf.contains("warnings:")
596    }
597
598    #[inline]
599    fn empty_stderr(&self) -> bool {
600        self.stdout.is_empty()
601    }
602
603    #[inline]
604    fn empty_stdout(&self) -> bool {
605        self.stdout.is_empty()
606    }
607
608    fn with_stdout_file<P: AsRef<Path>>(&self, filename: P) {
609        let expected = match std::fs::read_to_string(&filename) {
610            Ok(s) => s,
611            Err(e) => panic!("Couldn't read file {}: {e}", filename.as_ref().display()),
612        };
613
614        let mut buf = String::new();
615        buf.push_str(match str::from_utf8(&self.stdout) {
616            Ok(val) => val,
617            Err(_) => panic!("stdout isn't UTF-8 (bug)"),
618        });
619
620        assert_eq!(expected, buf);
621    }
622
623    fn with_stderr_file<P: AsRef<Path>>(&self, filename: P) {
624        let expected = match std::fs::read_to_string(&filename) {
625            Ok(s) => s,
626            Err(e) => panic!("Couldn't read file {}: {e}", filename.as_ref().display()),
627        };
628
629        let mut buf = String::new();
630        buf.push_str(match str::from_utf8(&self.stderr) {
631            Ok(val) => val,
632            Err(_) => panic!("stderr isn't UTF-8 (bug)"),
633        });
634
635        assert_eq!(expected, buf);
636    }
637}
638
639#[cfg(feature = "fuzz")]
640/// Generates a random string of text, meant to be used a mini-fuzz test. (As input to your CLI.)
641///
642/// ## Example
643///
644/// ```no_run
645/// # use cli_sandbox::{project, fuzz, WithStdout};
646/// # use std::error::Error;
647/// # fn main() -> Result<(), Box<dyn Error>> {
648/// let proj = project()?;
649/// let cmd = proj.command(["name", &fuzz(10)])?; // Use a random string of length 10
650/// cmd.with_stdout("...");
651/// # Ok(())
652/// # }
653/// ```
654pub fn fuzz(length: usize) -> String {
655    let charset = if let Ok(charset) = env::var("CARGO_CFG_FUZZ_CHARSET") {
656        charset
657    } else {
658        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".into()
659    };
660
661    let chars = charset.chars().collect::<Vec<char>>();
662
663    let mut buf = String::new();
664    for _ in 0..=length {
665        buf.push(chars[fastrand::usize(..charset.len())]);
666    }
667
668    buf
669}
670
671#[cfg(feature = "fuzz_seed")]
672/// Generates a random string of text, meant to be used a mini-fuzz test. (As input to your CLI.) It's different from [`fuzz`] because this function also takes a seed, meaining that it will output easily determinitable results.
673///
674/// ## Example
675///
676/// ```no_run
677/// # use cli_sandbox::{project, fuzz_seed, WithStdout};
678/// # use std::error::Error;
679/// # fn main() -> Result<(), Box<dyn Error>> {
680/// let proj = project()?;
681/// let cmd = proj.command(["name", &fuzz_seed(5, 10)])?; // Use a random string of length 10
682/// cmd.with_stdout("...");
683/// # Ok(())
684/// # }
685/// ```
686pub fn fuzz_seed(length: usize, seed: u64) -> String {
687    fastrand::seed(seed);
688    let charset = if let Ok(charset) = env::var("CARGO_CFG_FUZZ_CHARSET") {
689        charset
690    } else {
691        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".into()
692    };
693
694    let mut chars = charset.chars();
695
696    let mut buf = String::new();
697    for _ in 0..=length {
698        buf.push(
699            chars
700                .nth(fastrand::u8(..charset.len() as u8).into())
701                .unwrap(),
702        );
703    }
704
705    charset
706}
707
708pub const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");