1use std::collections::HashMap;
32use std::env;
33use std::fs;
34use std::io::{self, Read, Write};
35use std::panic::{catch_unwind, AssertUnwindSafe};
36use std::path::{Path, PathBuf};
37use std::sync::{Mutex, OnceLock};
38use std::thread;
39
40use crate::group::CommandLike;
41use encoding_rs::Encoding;
42
43#[derive(Debug)]
44enum CaptureOutcome {
45 Returned(Result<(), crate::ClickError>),
46 Panicked(Box<dyn std::any::Any + Send + 'static>),
47}
48
49fn panic_message(panic: &(dyn std::any::Any + Send + 'static)) -> String {
50 if let Some(s) = panic.downcast_ref::<&str>() {
51 (*s).to_string()
52 } else if let Some(s) = panic.downcast_ref::<String>() {
53 s.clone()
54 } else {
55 "panic".to_string()
56 }
57}
58
59fn restore_env(saved_env: &[(String, Option<String>)]) {
60 for (key, value) in saved_env {
61 match value {
62 Some(v) => env::set_var(key, v),
63 None => env::remove_var(key),
64 }
65 }
66}
67
68#[cfg(any(unix, windows))]
74static IO_CAPTURE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
75
76#[cfg(any(unix, windows))]
77fn capture_lock() -> std::sync::MutexGuard<'static, ()> {
78 IO_CAPTURE_LOCK
79 .get_or_init(|| Mutex::new(()))
80 .lock()
81 .expect("IO capture lock poisoned")
82}
83
84#[cfg(unix)]
85fn run_with_capture<F>(input: &str, f: F) -> io::Result<(CaptureOutcome, Vec<u8>, Vec<u8>)>
86where
87 F: FnOnce() -> Result<(), crate::ClickError>,
88{
89 use nix::unistd::{dup, dup2_stderr, dup2_stdin, dup2_stdout};
90
91 let _lock = capture_lock();
92
93 let (out_reader_pipe, out_writer_pipe) = os_pipe::pipe()?;
95 let (err_reader_pipe, err_writer_pipe) = os_pipe::pipe()?;
96 let (in_reader_pipe, in_writer_pipe) = os_pipe::pipe()?;
97
98 let saved_stdin = dup(io::stdin()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
100 let saved_stdout = dup(io::stdout()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
101 let saved_stderr = dup(io::stderr()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
102
103 {
105 let mut writer = in_writer_pipe;
106 let _ = writer.write_all(input.as_bytes());
107 }
108
109 let redir_ok = dup2_stdin(&in_reader_pipe)
111 .and_then(|_| dup2_stdout(&out_writer_pipe))
112 .and_then(|_| dup2_stderr(&err_writer_pipe))
113 .is_ok();
114
115 drop(in_reader_pipe);
117 drop(out_writer_pipe);
118 drop(err_writer_pipe);
119
120 if !redir_ok {
121 let _ = dup2_stdin(&saved_stdin);
122 let _ = dup2_stdout(&saved_stdout);
123 let _ = dup2_stderr(&saved_stderr);
124 return Err(io::Error::new(
125 io::ErrorKind::Other,
126 "failed to redirect stdio",
127 ));
128 }
129
130 let out_thread = thread::spawn(move || {
132 let mut buf = Vec::new();
133 let mut reader = out_reader_pipe;
134 let _ = reader.read_to_end(&mut buf);
135 buf
136 });
137 let err_thread = thread::spawn(move || {
138 let mut buf = Vec::new();
139 let mut reader = err_reader_pipe;
140 let _ = reader.read_to_end(&mut buf);
141 buf
142 });
143
144 let old_panic_hook = std::panic::take_hook();
145 std::panic::set_hook(Box::new(|_| {}));
146 let unwind = catch_unwind(AssertUnwindSafe(f));
147 std::panic::set_hook(old_panic_hook);
148
149 let _ = io::stdout().flush();
150 let _ = io::stderr().flush();
151
152 let _ = dup2_stdin(&saved_stdin);
154 let _ = dup2_stdout(&saved_stdout);
155 let _ = dup2_stderr(&saved_stderr);
156
157 let stdout_bytes = out_thread.join().unwrap_or_default();
158 let stderr_bytes = err_thread.join().unwrap_or_default();
159
160 let outcome = match unwind {
161 Ok(r) => CaptureOutcome::Returned(r),
162 Err(panic) => CaptureOutcome::Panicked(panic),
163 };
164
165 Ok((outcome, stdout_bytes, stderr_bytes))
166}
167
168#[cfg(windows)]
169fn run_with_capture<F>(input: &str, f: F) -> io::Result<(CaptureOutcome, Vec<u8>, Vec<u8>)>
170where
171 F: FnOnce() -> Result<(), crate::ClickError>,
172{
173 use std::os::windows::io::AsRawHandle;
174 use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
175 use windows_sys::Win32::System::Console::{
176 GetStdHandle, SetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
177 };
178
179 let _lock = capture_lock();
180
181 let (out_reader_pipe, out_writer_pipe) = os_pipe::pipe()?;
183 let (err_reader_pipe, err_writer_pipe) = os_pipe::pipe()?;
184 let (in_reader_pipe, in_writer_pipe) = os_pipe::pipe()?;
185
186 let (saved_in, saved_out, saved_err) = unsafe {
190 (
191 GetStdHandle(STD_INPUT_HANDLE),
192 GetStdHandle(STD_OUTPUT_HANDLE),
193 GetStdHandle(STD_ERROR_HANDLE),
194 )
195 };
196
197 if saved_in == 0
198 || saved_out == 0
199 || saved_err == 0
200 || saved_in == INVALID_HANDLE_VALUE
201 || saved_out == INVALID_HANDLE_VALUE
202 || saved_err == INVALID_HANDLE_VALUE
203 {
204 return Err(io::Error::new(io::ErrorKind::Other, "GetStdHandle failed"));
205 }
206
207 {
209 let mut writer = in_writer_pipe;
210 let _ = writer.write_all(input.as_bytes());
211 }
212
213 let redir_ok = unsafe {
217 SetStdHandle(STD_INPUT_HANDLE, in_reader_pipe.as_raw_handle() as _) != 0
218 && SetStdHandle(STD_OUTPUT_HANDLE, out_writer_pipe.as_raw_handle() as _) != 0
219 && SetStdHandle(STD_ERROR_HANDLE, err_writer_pipe.as_raw_handle() as _) != 0
220 };
221
222 if !redir_ok {
223 unsafe {
225 let _ = SetStdHandle(STD_INPUT_HANDLE, saved_in);
226 let _ = SetStdHandle(STD_OUTPUT_HANDLE, saved_out);
227 let _ = SetStdHandle(STD_ERROR_HANDLE, saved_err);
228 }
229 return Err(io::Error::last_os_error());
230 }
231
232 let out_thread = thread::spawn(move || {
234 let mut buf = Vec::new();
235 let mut reader = out_reader_pipe;
236 let _ = reader.read_to_end(&mut buf);
237 buf
238 });
239 let err_thread = thread::spawn(move || {
240 let mut buf = Vec::new();
241 let mut reader = err_reader_pipe;
242 let _ = reader.read_to_end(&mut buf);
243 buf
244 });
245
246 let old_panic_hook = std::panic::take_hook();
247 std::panic::set_hook(Box::new(|_| {}));
248 let unwind = catch_unwind(AssertUnwindSafe(f));
249 std::panic::set_hook(old_panic_hook);
250
251 let _ = io::stdout().flush();
252 let _ = io::stderr().flush();
253
254 unsafe {
257 let _ = SetStdHandle(STD_INPUT_HANDLE, saved_in);
258 let _ = SetStdHandle(STD_OUTPUT_HANDLE, saved_out);
259 let _ = SetStdHandle(STD_ERROR_HANDLE, saved_err);
260 }
261
262 drop(out_writer_pipe);
264 drop(err_writer_pipe);
265 drop(in_reader_pipe);
266
267 let stdout_bytes = out_thread.join().unwrap_or_default();
268 let stderr_bytes = err_thread.join().unwrap_or_default();
269
270 let outcome = match unwind {
271 Ok(r) => CaptureOutcome::Returned(r),
272 Err(panic) => CaptureOutcome::Panicked(panic),
273 };
274
275 Ok((outcome, stdout_bytes, stderr_bytes))
276}
277
278#[cfg(not(any(unix, windows)))]
279fn run_with_capture<F>(_input: &str, f: F) -> io::Result<(CaptureOutcome, Vec<u8>, Vec<u8>)>
280where
281 F: FnOnce() -> Result<(), crate::ClickError>,
282{
283 let unwind = catch_unwind(AssertUnwindSafe(f));
284 let outcome = match unwind {
285 Ok(r) => CaptureOutcome::Returned(r),
286 Err(panic) => CaptureOutcome::Panicked(panic),
287 };
288 Ok((outcome, Vec::new(), Vec::new()))
289}
290
291#[derive(Debug, Clone, Default)]
321pub struct CliRunner {
322 env: HashMap<String, String>,
324
325 env_unset: Vec<String>,
327
328 echo_stdin: bool,
330
331 mix_stderr: bool,
333
334 catch_panics: bool,
339
340 charset: String,
342}
343
344impl CliRunner {
345 pub fn new() -> Self {
355 Self {
356 env: HashMap::new(),
357 env_unset: Vec::new(),
358 echo_stdin: false,
359 mix_stderr: true,
360 catch_panics: true,
361 charset: "utf-8".to_string(),
362 }
363 }
364
365 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
376 self.env.insert(key.into(), value.into());
377 self
378 }
379
380 pub fn env_unset(mut self, key: impl Into<String>) -> Self {
382 self.env_unset.push(key.into());
383 self
384 }
385
386 pub fn env_clear(mut self) -> Self {
388 self.env.clear();
389 self.env_unset.clear();
390 self
391 }
392
393 pub fn echo_stdin(mut self, echo: bool) -> Self {
395 self.echo_stdin = echo;
396 self
397 }
398
399 pub fn mix_stderr(mut self, mix: bool) -> Self {
404 self.mix_stderr = mix;
405 self
406 }
407
408 pub fn catch_panics(mut self, catch: bool) -> Self {
410 self.catch_panics = catch;
411 self
412 }
413
414 pub fn charset(mut self, charset: impl Into<String>) -> Self {
418 self.charset = charset.into();
419 self
420 }
421
422 pub fn invoke(&self, cmd: &dyn CommandLike, args: &[&str]) -> InvokeResult {
446 self.invoke_with_input(cmd, args, None)
447 }
448
449 pub fn invoke_with_input(
470 &self,
471 cmd: &dyn CommandLike,
472 args: &[&str],
473 input: Option<&str>,
474 ) -> InvokeResult {
475 let saved_env: Vec<(String, Option<String>)> = self
477 .env
478 .keys()
479 .chain(self.env_unset.iter())
480 .map(|k| (k.clone(), env::var(k).ok()))
481 .collect();
482
483 for (key, value) in &self.env {
485 env::set_var(key, value);
486 }
487 for key in &self.env_unset {
488 env::remove_var(key);
489 }
490
491 let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
493
494 let input_str = input.unwrap_or("");
496
497 let (outcome, stdout_bytes, stderr_bytes) = match run_with_capture(input_str, || {
498 thread::scope(|s| {
501 let handle = s.spawn(|| cmd.main(args_owned));
502 match handle.join() {
503 Ok(r) => r,
504 Err(panic) => std::panic::resume_unwind(panic),
505 }
506 })
507 }) {
508 Ok(v) => v,
509 Err(e) => {
510 restore_env(&saved_env);
511 return InvokeResult::new(1, String::new(), String::new(), Some(e.to_string()));
512 }
513 };
514
515 let (exit_code, exception_message) = match outcome {
517 CaptureOutcome::Returned(r) => match &r {
518 Ok(()) => (0, None),
519 Err(e) => (e.exit_code(), Some(e.to_string())),
520 },
521 CaptureOutcome::Panicked(panic) => {
522 if !self.catch_panics {
523 restore_env(&saved_env);
524 std::panic::resume_unwind(panic);
525 }
526 (1, Some(format!("panic: {}", panic_message(&*panic))))
527 }
528 };
529
530 let label = self.charset.trim().to_lowercase();
531 let encoding = Encoding::for_label(label.as_bytes())
532 .or_else(|| {
533 if label == "latin-1" || label == "latin1" {
534 Encoding::for_label(b"iso-8859-1")
535 } else {
536 None
537 }
538 })
539 .unwrap_or(encoding_rs::UTF_8);
540 let mut stdout = encoding.decode(&stdout_bytes).0.into_owned();
541 let stderr = encoding.decode(&stderr_bytes).0.into_owned();
542
543 if self.echo_stdin && !input_str.is_empty() {
545 stdout = format!("{}{}", input_str, stdout);
546 }
547
548 let output = if self.mix_stderr {
549 format!("{}{}", stdout, stderr)
550 } else {
551 stdout
552 };
553
554 restore_env(&saved_env);
556
557 InvokeResult::new(exit_code, output, stderr, exception_message)
558 }
559
560 pub fn invoke_isolated(&self, cmd: &dyn CommandLike, args: &[&str]) -> InvokeResult {
579 let _isolated = IsolatedFilesystem::new().expect("Failed to create isolated filesystem");
580 self.invoke(cmd, args)
581 }
582}
583
584#[derive(Debug, Clone)]
592pub struct InvokeResult {
593 pub exit_code: i32,
595
596 pub output: String,
598
599 pub stderr: String,
601
602 pub exception_message: Option<String>,
604}
605
606impl InvokeResult {
607 #[doc(hidden)]
609 pub fn new(
610 exit_code: i32,
611 output: String,
612 stderr: String,
613 exception_message: Option<String>,
614 ) -> Self {
615 Self {
616 exit_code,
617 output,
618 stderr,
619 exception_message,
620 }
621 }
622
623 pub fn is_success(&self) -> bool {
625 self.exit_code == 0
626 }
627
628 pub fn is_failure(&self) -> bool {
630 self.exit_code != 0
631 }
632
633 pub fn output_lines(&self) -> Vec<&str> {
635 self.output.lines().collect()
636 }
637
638 pub fn output_contains(&self, substring: &str) -> bool {
640 self.output.contains(substring)
641 }
642
643 pub fn stderr_contains(&self, substring: &str) -> bool {
645 self.stderr.contains(substring)
646 }
647
648 pub fn combined_output(&self) -> String {
652 if self.stderr.is_empty() {
653 return self.output.clone();
654 }
655
656 if self.output.ends_with(&self.stderr) {
658 return self.output.clone();
659 }
660
661 format!("{}{}", self.output, self.stderr)
662 }
663}
664
665#[derive(Debug)]
693pub struct IsolatedFilesystem {
694 path: PathBuf,
696 original_cwd: PathBuf,
698}
699
700impl IsolatedFilesystem {
701 fn unique_suffix() -> u64 {
703 use std::sync::atomic::{AtomicU64, Ordering};
704 use std::time::{SystemTime, UNIX_EPOCH};
705 static COUNTER: AtomicU64 = AtomicU64::new(0);
706 let count = COUNTER.fetch_add(1, Ordering::SeqCst);
707 let timestamp = SystemTime::now()
708 .duration_since(UNIX_EPOCH)
709 .unwrap_or_default()
710 .as_nanos() as u64;
711 timestamp ^ count
712 }
713
714 fn get_current_dir_safe() -> io::Result<PathBuf> {
719 match env::current_dir() {
720 Ok(cwd) => Ok(cwd),
721 Err(_) => {
722 let temp = env::temp_dir();
724 let _ = env::set_current_dir(&temp);
725 Ok(temp)
726 }
727 }
728 }
729
730 pub fn new() -> io::Result<Self> {
734 let original_cwd = Self::get_current_dir_safe()?;
735 let path = env::temp_dir().join(format!(
736 "click_test_{}_{}",
737 std::process::id(),
738 Self::unique_suffix()
739 ));
740
741 fs::create_dir_all(&path)?;
743
744 env::set_current_dir(&path)?;
746
747 Ok(Self { path, original_cwd })
748 }
749
750 pub fn with_name(name: &str) -> io::Result<Self> {
752 let original_cwd = Self::get_current_dir_safe()?;
753 let path = env::temp_dir().join(format!(
754 "click_test_{}_{}_{}",
755 name,
756 std::process::id(),
757 Self::unique_suffix()
758 ));
759
760 fs::create_dir_all(&path)?;
762 env::set_current_dir(&path)?;
763
764 Ok(Self { path, original_cwd })
765 }
766
767 pub fn path(&self) -> &Path {
769 &self.path
770 }
771
772 pub fn create_file(&self, name: &str, content: &str) -> io::Result<PathBuf> {
774 let file_path = self.path.join(name);
775 if let Some(parent) = file_path.parent() {
776 fs::create_dir_all(parent)?;
777 }
778 fs::write(&file_path, content)?;
779 Ok(file_path)
780 }
781
782 pub fn create_dir(&self, name: &str) -> io::Result<PathBuf> {
784 let dir_path = self.path.join(name);
785 fs::create_dir_all(&dir_path)?;
786 Ok(dir_path)
787 }
788
789 pub fn read_file(&self, name: &str) -> io::Result<String> {
791 fs::read_to_string(self.path.join(name))
792 }
793
794 pub fn file_exists(&self, name: &str) -> bool {
796 self.path.join(name).exists()
797 }
798
799 pub fn list_files(&self) -> io::Result<Vec<String>> {
801 let mut files = Vec::new();
802 for entry in fs::read_dir(&self.path)? {
803 let entry = entry?;
804 if let Some(name) = entry.file_name().to_str() {
805 files.push(name.to_string());
806 }
807 }
808 files.sort();
809 Ok(files)
810 }
811}
812
813impl Drop for IsolatedFilesystem {
814 fn drop(&mut self) {
815 let _ = env::set_current_dir(&self.original_cwd);
817
818 let _ = fs::remove_dir_all(&self.path);
820 }
821}
822
823#[derive(Debug)]
829pub struct EchoingStdin<R: Read, W: Write> {
830 input: R,
831 output: W,
832}
833
834impl<R: Read, W: Write> EchoingStdin<R, W> {
835 pub fn new(input: R, output: W) -> Self {
837 Self { input, output }
838 }
839}
840
841impl<R: Read, W: Write> Read for EchoingStdin<R, W> {
842 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
843 let n = self.input.read(buf)?;
844 if n > 0 {
845 self.output.write_all(&buf[..n])?;
846 }
847 Ok(n)
848 }
849}
850
851pub fn make_test_context(info_name: &str) -> crate::context::Context {
859 crate::context::ContextBuilder::new()
860 .info_name(info_name)
861 .build()
862}
863
864#[macro_export]
866macro_rules! assert_success {
867 ($result:expr) => {
868 assert!(
869 $result.is_success(),
870 "Expected success but got exit code {} with output:\n{}",
871 $result.exit_code,
872 $result.combined_output()
873 );
874 };
875}
876
877#[macro_export]
879macro_rules! assert_failure {
880 ($result:expr) => {
881 assert!(
882 $result.is_failure(),
883 "Expected failure but got success with output:\n{}",
884 $result.output
885 );
886 };
887 ($result:expr, $code:expr) => {
888 assert_eq!(
889 $result.exit_code,
890 $code,
891 "Expected exit code {} but got {} with output:\n{}",
892 $code,
893 $result.exit_code,
894 $result.combined_output()
895 );
896 };
897}
898
899#[macro_export]
901macro_rules! assert_output_contains {
902 ($result:expr, $substring:expr) => {
903 assert!(
904 $result.output_contains($substring),
905 "Expected output to contain '{}' but got:\n{}",
906 $substring,
907 $result.output
908 );
909 };
910}
911
912#[cfg(test)]
917mod tests {
918 use super::*;
919 use crate::command::Command;
920
921 #[test]
922 fn test_cli_runner_new() {
923 let runner = CliRunner::new();
924 assert!(runner.env.is_empty());
925 assert!(runner.env_unset.is_empty());
926 }
927
928 #[test]
929 fn test_cli_runner_env() {
930 let runner = CliRunner::new()
931 .env("TEST_VAR", "test_value")
932 .env("ANOTHER", "value");
933
934 assert_eq!(runner.env.get("TEST_VAR"), Some(&"test_value".to_string()));
935 assert_eq!(runner.env.get("ANOTHER"), Some(&"value".to_string()));
936 }
937
938 #[test]
939 fn test_cli_runner_env_unset() {
940 let runner = CliRunner::new().env("KEEP", "value").env_unset("REMOVE");
941
942 assert_eq!(runner.env.len(), 1);
943 assert_eq!(runner.env_unset.len(), 1);
944 }
945
946 #[test]
947 fn test_cli_runner_env_clear() {
948 let runner = CliRunner::new()
949 .env("VAR1", "val1")
950 .env("VAR2", "val2")
951 .env_unset("VAR3")
952 .env_clear();
953
954 assert!(runner.env.is_empty());
955 assert!(runner.env_unset.is_empty());
956 }
957
958 #[test]
959 fn test_invoke_simple_command() {
960 let cmd = Command::new("test").callback(|_ctx| Ok(())).build();
961
962 let runner = CliRunner::new();
963 let result = runner.invoke(&cmd, &[]);
964
965 assert_eq!(result.exit_code, 0);
966 assert!(result.exception_message.is_none());
967 }
968
969 #[test]
970 fn test_invoke_failing_command() {
971 let cmd = Command::new("fail")
972 .callback(|_ctx| Err(crate::ClickError::usage("test error")))
973 .build();
974
975 let runner = CliRunner::new();
976 let result = runner.invoke(&cmd, &[]);
977
978 assert_eq!(result.exit_code, 2); assert!(result.exception_message.is_some());
980 }
981
982 #[test]
983 fn test_invoke_result_is_success() {
984 let result = InvokeResult::new(0, String::new(), String::new(), None);
985
986 assert!(result.is_success());
987 assert!(!result.is_failure());
988 }
989
990 #[test]
991 fn test_invoke_result_is_failure() {
992 let result = InvokeResult::new(1, String::new(), String::new(), None);
993
994 assert!(!result.is_success());
995 assert!(result.is_failure());
996 }
997
998 #[test]
999 fn test_invoke_result_output_contains() {
1000 let result = InvokeResult::new(0, "Hello, World!".to_string(), String::new(), None);
1001
1002 assert!(result.output_contains("Hello"));
1003 assert!(result.output_contains("World"));
1004 assert!(!result.output_contains("Goodbye"));
1005 }
1006
1007 #[test]
1008 fn test_invoke_result_output_lines() {
1009 let result = InvokeResult::new(0, "line1\nline2\nline3".to_string(), String::new(), None);
1010
1011 let lines = result.output_lines();
1012 assert_eq!(lines.len(), 3);
1013 assert_eq!(lines[0], "line1");
1014 assert_eq!(lines[1], "line2");
1015 assert_eq!(lines[2], "line3");
1016 }
1017
1018 #[test]
1019 fn test_invoke_result_combined_output() {
1020 let result = InvokeResult::new(0, "stdout".to_string(), "stderr".to_string(), None);
1021
1022 assert_eq!(result.combined_output(), "stdoutstderr");
1023 }
1024
1025 #[test]
1026 fn test_isolated_filesystem() {
1027 let isolated = IsolatedFilesystem::new().unwrap();
1028 let iso_path = isolated.path().to_path_buf();
1029
1030 assert!(iso_path.exists());
1032 assert!(iso_path.starts_with(env::temp_dir()));
1033
1034 isolated.create_file("test.txt", "hello").unwrap();
1036 assert!(isolated.file_exists("test.txt"));
1037 assert_eq!(isolated.read_file("test.txt").unwrap(), "hello");
1038
1039 drop(isolated);
1041
1042 assert!(
1045 !iso_path.exists(),
1046 "temp directory should be cleaned up after drop"
1047 );
1048 }
1049
1050 #[test]
1051 fn test_isolated_filesystem_with_name() {
1052 let isolated = IsolatedFilesystem::with_name("custom").unwrap();
1053 let path = isolated.path();
1054
1055 assert!(path.to_string_lossy().contains("custom"));
1056 }
1057
1058 #[test]
1059 fn test_isolated_filesystem_create_dir() {
1060 let isolated = IsolatedFilesystem::new().unwrap();
1061
1062 let dir_path = isolated.create_dir("subdir").unwrap();
1063 assert!(dir_path.exists());
1064 assert!(dir_path.is_dir());
1065 }
1066
1067 #[test]
1068 fn test_isolated_filesystem_list_files() {
1069 let isolated = IsolatedFilesystem::new().unwrap();
1070
1071 isolated.create_file("a.txt", "").unwrap();
1072 isolated.create_file("b.txt", "").unwrap();
1073 isolated.create_file("c.txt", "").unwrap();
1074
1075 let files = isolated.list_files().unwrap();
1076 assert_eq!(files, vec!["a.txt", "b.txt", "c.txt"]);
1077 }
1078
1079 #[test]
1080 fn test_isolated_filesystem_nested_file() {
1081 let isolated = IsolatedFilesystem::new().unwrap();
1082
1083 isolated
1084 .create_file("dir/nested/file.txt", "content")
1085 .unwrap();
1086 assert!(isolated.file_exists("dir/nested/file.txt"));
1087 assert_eq!(
1088 isolated.read_file("dir/nested/file.txt").unwrap(),
1089 "content"
1090 );
1091 }
1092
1093 #[test]
1094 fn test_make_test_context() {
1095 let ctx = make_test_context("test");
1096 assert_eq!(ctx.info_name(), Some("test"));
1097 }
1098
1099 #[test]
1100 fn test_echoing_stdin() {
1101 let input = b"hello";
1102 let mut output = Vec::new();
1103
1104 {
1105 let mut echoing = EchoingStdin::new(&input[..], &mut output);
1106 let mut buf = [0u8; 10];
1107 let n = echoing.read(&mut buf).unwrap();
1108 assert_eq!(n, 5);
1109 assert_eq!(&buf[..n], b"hello");
1110 }
1111
1112 assert_eq!(&output, b"hello");
1113 }
1114
1115 #[test]
1116 fn test_cli_runner_mix_stderr() {
1117 let runner = CliRunner::new().mix_stderr(false);
1118 assert!(!runner.mix_stderr);
1119
1120 let runner = CliRunner::new().mix_stderr(true);
1121 assert!(runner.mix_stderr);
1122 }
1123
1124 #[test]
1125 fn test_cli_runner_echo_stdin() {
1126 let runner = CliRunner::new().echo_stdin(true);
1127 assert!(runner.echo_stdin);
1128 }
1129}