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}
344
345impl CliRunner {
346 pub fn new() -> Self {
356 Self {
357 env: HashMap::new(),
358 env_unset: Vec::new(),
359 echo_stdin: false,
360 mix_stderr: true,
361 catch_panics: true,
362 charset: "utf-8".to_string(),
363 }
364 }
365
366 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
377 self.env.insert(key.into(), value.into());
378 self
379 }
380
381 pub fn env_unset(mut self, key: impl Into<String>) -> Self {
383 self.env_unset.push(key.into());
384 self
385 }
386
387 pub fn env_clear(mut self) -> Self {
389 self.env.clear();
390 self.env_unset.clear();
391 self
392 }
393
394 pub fn echo_stdin(mut self, echo: bool) -> Self {
396 self.echo_stdin = echo;
397 self
398 }
399
400 pub fn mix_stderr(mut self, mix: bool) -> Self {
405 self.mix_stderr = mix;
406 self
407 }
408
409 pub fn catch_panics(mut self, catch: bool) -> Self {
411 self.catch_panics = catch;
412 self
413 }
414
415 pub fn charset(mut self, charset: impl Into<String>) -> Self {
419 self.charset = charset.into();
420 self
421 }
422
423 pub fn invoke(&self, cmd: &dyn CommandLike, args: &[&str]) -> InvokeResult {
447 self.invoke_with_input(cmd, args, None)
448 }
449
450 pub fn invoke_with_input(
471 &self,
472 cmd: &dyn CommandLike,
473 args: &[&str],
474 input: Option<&str>,
475 ) -> InvokeResult {
476 let saved_env: Vec<(String, Option<String>)> = self
478 .env
479 .keys()
480 .chain(self.env_unset.iter())
481 .map(|k| (k.clone(), env::var(k).ok()))
482 .collect();
483
484 for (key, value) in &self.env {
486 env::set_var(key, value);
487 }
488 for key in &self.env_unset {
489 env::remove_var(key);
490 }
491
492 let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
494
495 let input_str = input.unwrap_or("");
497
498 let (outcome, stdout_bytes, stderr_bytes) = match run_with_capture(input_str, || {
499 thread::scope(|s| {
502 let handle = s.spawn(|| cmd.main(args_owned));
503 match handle.join() {
504 Ok(r) => r,
505 Err(panic) => std::panic::resume_unwind(panic),
506 }
507 })
508 }) {
509 Ok(v) => v,
510 Err(e) => {
511 restore_env(&saved_env);
512 return InvokeResult::new(1, String::new(), String::new(), Some(e.to_string()));
513 }
514 };
515
516 let (exit_code, exception_message) = match outcome {
518 CaptureOutcome::Returned(r) => match &r {
519 Ok(()) => (0, None),
520 Err(e) => (e.exit_code(), Some(e.to_string())),
521 },
522 CaptureOutcome::Panicked(panic) => {
523 if !self.catch_panics {
524 restore_env(&saved_env);
525 std::panic::resume_unwind(panic);
526 }
527 (1, Some(format!("panic: {}", panic_message(&*panic))))
528 }
529 };
530
531 let label = self.charset.trim().to_lowercase();
532 let encoding = Encoding::for_label(label.as_bytes())
533 .or_else(|| {
534 if label == "latin-1" || label == "latin1" {
535 Encoding::for_label(b"iso-8859-1")
536 } else {
537 None
538 }
539 })
540 .unwrap_or(encoding_rs::UTF_8);
541 let mut stdout = encoding.decode(&stdout_bytes).0.into_owned();
542 let stderr = encoding.decode(&stderr_bytes).0.into_owned();
543
544 if self.echo_stdin && !input_str.is_empty() {
546 stdout = format!("{}{}", input_str, stdout);
547 }
548
549 let output = if self.mix_stderr {
550 format!("{}{}", stdout, stderr)
551 } else {
552 stdout
553 };
554
555 restore_env(&saved_env);
557
558 InvokeResult::new(exit_code, output, stderr, exception_message)
559 }
560
561 pub fn invoke_isolated(&self, cmd: &dyn CommandLike, args: &[&str]) -> InvokeResult {
580 let _isolated = IsolatedFilesystem::new().expect("Failed to create isolated filesystem");
581 self.invoke(cmd, args)
582 }
583}
584
585#[derive(Debug, Clone)]
593pub struct InvokeResult {
594 pub exit_code: i32,
596
597 pub output: String,
599
600 pub stderr: String,
602
603 pub exception_message: Option<String>,
605}
606
607impl InvokeResult {
608 #[doc(hidden)]
610 pub fn new(
611 exit_code: i32,
612 output: String,
613 stderr: String,
614 exception_message: Option<String>,
615 ) -> Self {
616 Self {
617 exit_code,
618 output,
619 stderr,
620 exception_message,
621 }
622 }
623
624 pub fn is_success(&self) -> bool {
626 self.exit_code == 0
627 }
628
629 pub fn is_failure(&self) -> bool {
631 self.exit_code != 0
632 }
633
634 pub fn output_lines(&self) -> Vec<&str> {
636 self.output.lines().collect()
637 }
638
639 pub fn output_contains(&self, substring: &str) -> bool {
641 self.output.contains(substring)
642 }
643
644 pub fn stderr_contains(&self, substring: &str) -> bool {
646 self.stderr.contains(substring)
647 }
648
649 pub fn combined_output(&self) -> String {
653 if self.stderr.is_empty() {
654 return self.output.clone();
655 }
656
657 if self.output.ends_with(&self.stderr) {
659 return self.output.clone();
660 }
661
662 format!("{}{}", self.output, self.stderr)
663 }
664}
665
666#[derive(Debug)]
694pub struct IsolatedFilesystem {
695 path: PathBuf,
697 original_cwd: PathBuf,
699}
700
701impl IsolatedFilesystem {
702 fn unique_suffix() -> u64 {
704 use std::sync::atomic::{AtomicU64, Ordering};
705 use std::time::{SystemTime, UNIX_EPOCH};
706 static COUNTER: AtomicU64 = AtomicU64::new(0);
707 let count = COUNTER.fetch_add(1, Ordering::SeqCst);
708 let timestamp = SystemTime::now()
709 .duration_since(UNIX_EPOCH)
710 .unwrap_or_default()
711 .as_nanos() as u64;
712 timestamp ^ count
713 }
714
715 fn get_current_dir_safe() -> io::Result<PathBuf> {
720 match env::current_dir() {
721 Ok(cwd) => Ok(cwd),
722 Err(_) => {
723 let temp = env::temp_dir();
725 let _ = env::set_current_dir(&temp);
726 Ok(temp)
727 }
728 }
729 }
730
731 pub fn new() -> io::Result<Self> {
735 let original_cwd = Self::get_current_dir_safe()?;
736 let path = env::temp_dir().join(format!(
737 "click_test_{}_{}",
738 std::process::id(),
739 Self::unique_suffix()
740 ));
741
742 fs::create_dir_all(&path)?;
744
745 env::set_current_dir(&path)?;
747
748 Ok(Self { path, original_cwd })
749 }
750
751 pub fn with_name(name: &str) -> io::Result<Self> {
753 let original_cwd = Self::get_current_dir_safe()?;
754 let path = env::temp_dir().join(format!(
755 "click_test_{}_{}_{}",
756 name,
757 std::process::id(),
758 Self::unique_suffix()
759 ));
760
761 fs::create_dir_all(&path)?;
763 env::set_current_dir(&path)?;
764
765 Ok(Self { path, original_cwd })
766 }
767
768 pub fn path(&self) -> &Path {
770 &self.path
771 }
772
773 pub fn create_file(&self, name: &str, content: &str) -> io::Result<PathBuf> {
775 let file_path = self.path.join(name);
776 if let Some(parent) = file_path.parent() {
777 fs::create_dir_all(parent)?;
778 }
779 fs::write(&file_path, content)?;
780 Ok(file_path)
781 }
782
783 pub fn create_dir(&self, name: &str) -> io::Result<PathBuf> {
785 let dir_path = self.path.join(name);
786 fs::create_dir_all(&dir_path)?;
787 Ok(dir_path)
788 }
789
790 pub fn read_file(&self, name: &str) -> io::Result<String> {
792 fs::read_to_string(self.path.join(name))
793 }
794
795 pub fn file_exists(&self, name: &str) -> bool {
797 self.path.join(name).exists()
798 }
799
800 pub fn list_files(&self) -> io::Result<Vec<String>> {
802 let mut files = Vec::new();
803 for entry in fs::read_dir(&self.path)? {
804 let entry = entry?;
805 if let Some(name) = entry.file_name().to_str() {
806 files.push(name.to_string());
807 }
808 }
809 files.sort();
810 Ok(files)
811 }
812}
813
814impl Drop for IsolatedFilesystem {
815 fn drop(&mut self) {
816 let _ = env::set_current_dir(&self.original_cwd);
818
819 let _ = fs::remove_dir_all(&self.path);
821 }
822}
823
824#[derive(Debug)]
830pub struct EchoingStdin<R: Read, W: Write> {
831 input: R,
832 output: W,
833}
834
835impl<R: Read, W: Write> EchoingStdin<R, W> {
836 pub fn new(input: R, output: W) -> Self {
838 Self { input, output }
839 }
840}
841
842impl<R: Read, W: Write> Read for EchoingStdin<R, W> {
843 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
844 let n = self.input.read(buf)?;
845 if n > 0 {
846 self.output.write_all(&buf[..n])?;
847 }
848 Ok(n)
849 }
850}
851
852pub fn make_test_context(info_name: &str) -> crate::context::Context {
860 crate::context::ContextBuilder::new()
861 .info_name(info_name)
862 .build()
863}
864
865#[macro_export]
867macro_rules! assert_success {
868 ($result:expr) => {
869 assert!(
870 $result.is_success(),
871 "Expected success but got exit code {} with output:\n{}",
872 $result.exit_code,
873 $result.combined_output()
874 );
875 };
876}
877
878#[macro_export]
880macro_rules! assert_failure {
881 ($result:expr) => {
882 assert!(
883 $result.is_failure(),
884 "Expected failure but got success with output:\n{}",
885 $result.output
886 );
887 };
888 ($result:expr, $code:expr) => {
889 assert_eq!(
890 $result.exit_code,
891 $code,
892 "Expected exit code {} but got {} with output:\n{}",
893 $code,
894 $result.exit_code,
895 $result.combined_output()
896 );
897 };
898}
899
900#[macro_export]
902macro_rules! assert_output_contains {
903 ($result:expr, $substring:expr) => {
904 assert!(
905 $result.output_contains($substring),
906 "Expected output to contain '{}' but got:\n{}",
907 $substring,
908 $result.output
909 );
910 };
911}
912
913#[cfg(test)]
918mod tests {
919 use super::*;
920 use crate::command::Command;
921
922 #[test]
923 fn test_cli_runner_new() {
924 let runner = CliRunner::new();
925 assert!(runner.env.is_empty());
926 assert!(runner.env_unset.is_empty());
927 }
928
929 #[test]
930 fn test_cli_runner_env() {
931 let runner = CliRunner::new()
932 .env("TEST_VAR", "test_value")
933 .env("ANOTHER", "value");
934
935 assert_eq!(runner.env.get("TEST_VAR"), Some(&"test_value".to_string()));
936 assert_eq!(runner.env.get("ANOTHER"), Some(&"value".to_string()));
937 }
938
939 #[test]
940 fn test_cli_runner_env_unset() {
941 let runner = CliRunner::new().env("KEEP", "value").env_unset("REMOVE");
942
943 assert_eq!(runner.env.len(), 1);
944 assert_eq!(runner.env_unset.len(), 1);
945 }
946
947 #[test]
948 fn test_cli_runner_env_clear() {
949 let runner = CliRunner::new()
950 .env("VAR1", "val1")
951 .env("VAR2", "val2")
952 .env_unset("VAR3")
953 .env_clear();
954
955 assert!(runner.env.is_empty());
956 assert!(runner.env_unset.is_empty());
957 }
958
959 #[test]
960 fn test_invoke_simple_command() {
961 let cmd = Command::new("test").callback(|_ctx| Ok(())).build();
962
963 let runner = CliRunner::new();
964 let result = runner.invoke(&cmd, &[]);
965
966 assert_eq!(result.exit_code, 0);
967 assert!(result.exception_message.is_none());
968 }
969
970 #[test]
971 fn test_invoke_failing_command() {
972 let cmd = Command::new("fail")
973 .callback(|_ctx| Err(crate::ClickError::usage("test error")))
974 .build();
975
976 let runner = CliRunner::new();
977 let result = runner.invoke(&cmd, &[]);
978
979 assert_eq!(result.exit_code, 2); assert!(result.exception_message.is_some());
981 }
982
983 #[test]
984 fn test_invoke_result_is_success() {
985 let result = InvokeResult::new(0, String::new(), String::new(), None);
986
987 assert!(result.is_success());
988 assert!(!result.is_failure());
989 }
990
991 #[test]
992 fn test_invoke_result_is_failure() {
993 let result = InvokeResult::new(1, String::new(), String::new(), None);
994
995 assert!(!result.is_success());
996 assert!(result.is_failure());
997 }
998
999 #[test]
1000 fn test_invoke_result_output_contains() {
1001 let result = InvokeResult::new(0, "Hello, World!".to_string(), String::new(), None);
1002
1003 assert!(result.output_contains("Hello"));
1004 assert!(result.output_contains("World"));
1005 assert!(!result.output_contains("Goodbye"));
1006 }
1007
1008 #[test]
1009 fn test_invoke_result_output_lines() {
1010 let result = InvokeResult::new(0, "line1\nline2\nline3".to_string(), String::new(), None);
1011
1012 let lines = result.output_lines();
1013 assert_eq!(lines.len(), 3);
1014 assert_eq!(lines[0], "line1");
1015 assert_eq!(lines[1], "line2");
1016 assert_eq!(lines[2], "line3");
1017 }
1018
1019 #[test]
1020 fn test_invoke_result_combined_output() {
1021 let result = InvokeResult::new(0, "stdout".to_string(), "stderr".to_string(), None);
1022
1023 assert_eq!(result.combined_output(), "stdoutstderr");
1024 }
1025
1026 #[test]
1027 fn test_isolated_filesystem() {
1028 let isolated = IsolatedFilesystem::new().unwrap();
1029 let iso_path = isolated.path().to_path_buf();
1030
1031 assert!(iso_path.exists());
1033 assert!(iso_path.starts_with(env::temp_dir()));
1034
1035 isolated.create_file("test.txt", "hello").unwrap();
1037 assert!(isolated.file_exists("test.txt"));
1038 assert_eq!(isolated.read_file("test.txt").unwrap(), "hello");
1039
1040 drop(isolated);
1042
1043 assert!(
1046 !iso_path.exists(),
1047 "temp directory should be cleaned up after drop"
1048 );
1049 }
1050
1051 #[test]
1052 fn test_isolated_filesystem_with_name() {
1053 let isolated = IsolatedFilesystem::with_name("custom").unwrap();
1054 let path = isolated.path();
1055
1056 assert!(path.to_string_lossy().contains("custom"));
1057 }
1058
1059 #[test]
1060 fn test_isolated_filesystem_create_dir() {
1061 let isolated = IsolatedFilesystem::new().unwrap();
1062
1063 let dir_path = isolated.create_dir("subdir").unwrap();
1064 assert!(dir_path.exists());
1065 assert!(dir_path.is_dir());
1066 }
1067
1068 #[test]
1069 fn test_isolated_filesystem_list_files() {
1070 let isolated = IsolatedFilesystem::new().unwrap();
1071
1072 isolated.create_file("a.txt", "").unwrap();
1073 isolated.create_file("b.txt", "").unwrap();
1074 isolated.create_file("c.txt", "").unwrap();
1075
1076 let files = isolated.list_files().unwrap();
1077 assert_eq!(files, vec!["a.txt", "b.txt", "c.txt"]);
1078 }
1079
1080 #[test]
1081 fn test_isolated_filesystem_nested_file() {
1082 let isolated = IsolatedFilesystem::new().unwrap();
1083
1084 isolated
1085 .create_file("dir/nested/file.txt", "content")
1086 .unwrap();
1087 assert!(isolated.file_exists("dir/nested/file.txt"));
1088 assert_eq!(
1089 isolated.read_file("dir/nested/file.txt").unwrap(),
1090 "content"
1091 );
1092 }
1093
1094 #[test]
1095 fn test_make_test_context() {
1096 let ctx = make_test_context("test");
1097 assert_eq!(ctx.info_name(), Some("test"));
1098 }
1099
1100 #[test]
1101 fn test_echoing_stdin() {
1102 let input = b"hello";
1103 let mut output = Vec::new();
1104
1105 {
1106 let mut echoing = EchoingStdin::new(&input[..], &mut output);
1107 let mut buf = [0u8; 10];
1108 let n = echoing.read(&mut buf).unwrap();
1109 assert_eq!(n, 5);
1110 assert_eq!(&buf[..n], b"hello");
1111 }
1112
1113 assert_eq!(&output, b"hello");
1114 }
1115
1116 #[test]
1117 fn test_cli_runner_mix_stderr() {
1118 let runner = CliRunner::new().mix_stderr(false);
1119 assert!(!runner.mix_stderr);
1120
1121 let runner = CliRunner::new().mix_stderr(true);
1122 assert!(runner.mix_stderr);
1123 }
1124
1125 #[test]
1126 fn test_cli_runner_echo_stdin() {
1127 let runner = CliRunner::new().echo_stdin(true);
1128 assert!(runner.echo_stdin);
1129 }
1130}