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");