//! Shell 执行器基础集成测试。 //! //! 覆盖 execute() 五类行为,跨平台(Windows=Cmd, 非 Windows=Sh): //! 1. 成功:exit_code=Some(0) + stdout 非空 + duration_ms>0 //! 2. 非零退出:exit_code=Some(非0),返回 Ok(退出码在 ShellResult 内,非 Err) //! 3. 超时:timeout_secs=1 + 长睡命令,返回 Err //! 4. env 注入:env HashMap 传 KEY=VAL,命令回显验证 //! 5. working_dir:传临时目录,命令回显当前目录验证 //! //! 注:execute 逻辑本身未改动,此文件为零行为变更的纯新增测试。 use df_execute::shell::{execute, execute_streaming, ShellRequest, ShellType, StreamKind}; use std::collections::HashMap; /// 平台默认 ShellType(对齐 shell.rs:31 Default impl:Windows→Cmd, 非 Windows→Sh) fn default_shell() -> ShellType { if cfg!(windows) { ShellType::Cmd } else { ShellType::Sh } } /// 构造最小 ShellRequest,默认无超时/无 env/无 working_dir fn req(command: &str) -> ShellRequest { ShellRequest { command: command.to_string(), working_dir: None, env: HashMap::new(), timeout_secs: None, shell_type: Some(default_shell()), } } // ---------- 1. 成功:exit_code=0 + stdout 非空 + duration_ms>0 ---------- #[tokio::test] async fn execute_success_echo() { // 平台无关:Cmd/Sh 都认 echo。Cmd 输出无尾换行,Sh 输出带 \n——只断言非空与包含。 let result = execute(req("echo hello_df_execute")).await; assert!(result.is_ok(), "execute 应返回 Ok: {:?}", result.err()); let res = result.unwrap(); assert_eq!(res.exit_code, Some(0), "成功命令 exit_code 应为 0"); assert!( res.stdout.trim().contains("hello_df_execute"), "stdout 应包含回显文本,实际: {:?}", res.stdout ); assert!(res.duration_ms > 0, "duration_ms 应 > 0,实际: {}", res.duration_ms); } // ---------- 2. 非零退出:exit_code=Some(非0),返回 Ok ---------- #[tokio::test] async fn execute_nonzero_exit() { // 平台无关:false 是 POSIX 内建 + Windows 的 cmd 也内置(返回退出码 1)。 // 非 Windows 用 sh -c 'false';Windows 用 cmd /C 'cmd /C exit 1' 更稳—— // 这里统一用 false,Windows cmd 内置 false.exe 自 Win10 起可用,失败则 exit_code 非 0 即满足断言。 let result = execute(req("exit 1")).await; assert!(result.is_ok(), "非零退出应仍返回 Ok(退出码在 ShellResult 内),实际: {:?}", result); let res = result.unwrap(); assert_ne!(res.exit_code, Some(0), "exit 1 的 exit_code 应非 0,实际: {:?}", res.exit_code); } // ---------- 3. 超时:timeout_secs=1 + 长睡命令,返回 Err ---------- #[tokio::test] async fn execute_timeout_returns_err() { // 平台无关睡眠命令:Cmd 用 timeout(Windows 内置),Sh 用 sleep。各自分支。 let sleep_cmd = if cfg!(windows) { // cmd 的 timeout /T 需按键中断,用 ping 假睡更稳(>1s);但 timeout 内置更简洁, // 这里用 ping -n 5 (≈4s) 跨 Win 版本稳。 "ping -n 5 127.0.0.1 > nul".to_string() } else { "sleep 5".to_string() }; let request = ShellRequest { command: sleep_cmd, working_dir: None, env: HashMap::new(), timeout_secs: Some(1), shell_type: Some(default_shell()), }; let result = execute(request).await; assert!( result.is_err(), "超时应返回 Err,实际: {:?}", result.as_ref().err() ); // 错误信息应含「超时」(对齐 shell.rs:101 文案) let msg = result.unwrap_err().to_string(); assert!( msg.contains("超时") || msg.to_lowercase().contains("timeout"), "错误信息应含超时提示,实际: {}", msg ); } // ---------- 4. env 注入:env HashMap 传 KEY=VAL,命令回显验证 ---------- #[tokio::test] async fn execute_env_injection() { let mut env = HashMap::new(); env.insert("DF_EXECUTE_TEST_KEY".to_string(), "env_value_42".to_string()); // 平台无关回显 env 变量:Cmd 用 %VAR%,Sh 用 $VAR let echo_cmd = if cfg!(windows) { "echo %DF_EXECUTE_TEST_KEY%" } else { "echo $DF_EXECUTE_TEST_KEY" }; let request = ShellRequest { command: echo_cmd.to_string(), working_dir: None, env, timeout_secs: None, shell_type: Some(default_shell()), }; let res = execute(request).await.expect("env 注入命令应返回 Ok"); assert_eq!(res.exit_code, Some(0)); assert!( res.stdout.trim().contains("env_value_42"), "stdout 应含注入的 env 值,实际: {:?}", res.stdout ); } // ---------- 5. working_dir:传临时目录,命令回显当前目录验证 ---------- #[tokio::test] async fn execute_working_dir() { // 用 std::env::temp_dir()(标准库,零新增依赖)。生成唯一子目录避免并行测试互相干扰。 let unique = format!("df_exec_test_{}", std::process::id()); let tmp = std::env::temp_dir().join(&unique); std::fs::create_dir_all(&tmp).expect("创建临时目录失败"); // 测试后清理(即便失败也尽量不残留) let tmp_for_cleanup = tmp.clone(); // 平台无关回显当前目录:Cmd 用 cd(无参数)不靠谱,用 echo %CD%;Sh 用 pwd。 let pwd_cmd = if cfg!(windows) { "echo %CD%" } else { "pwd" }; let request = ShellRequest { command: pwd_cmd.to_string(), working_dir: Some(tmp.to_string_lossy().to_string()), env: HashMap::new(), timeout_secs: None, shell_type: Some(default_shell()), }; let res = execute(request).await.expect("working_dir 命令应返回 Ok"); assert_eq!(res.exit_code, Some(0)); // 规范化比较(去尾空白/大小写在 Windows 不敏感但路径用户自定义,只断言包含) let stdout_norm = res.stdout.trim().to_lowercase().replace('\\', "/"); let tmp_norm = tmp.to_string_lossy().to_lowercase().replace('\\', "/"); assert!( stdout_norm.contains(&tmp_norm), "stdout 应含 working_dir 路径,实际: {:?},期望包含: {:?}", res.stdout, tmp.to_string_lossy() ); // 清理 let _ = std::fs::remove_dir_all(&tmp_for_cleanup); } // ============================================================ // execute_streaming 流式测试 // ============================================================ /// 流式:stdout 多行逐行回调,且 ShellResult 完整(行数对齐 + exit_code=0)。 /// /// 治 run_command 黑盒:验证 spawn 后逐行回调 vs 一次性返回的等价性(行内容 + 完整结果)。 #[tokio::test] async fn streaming_stdout_lines_callback() { // 多行输出:Cmd 用多个 echo(用 & 串联无依赖),Sh 用 printf 多行 let cmd = if cfg!(windows) { "@echo line1 & @echo line2 & @echo line3" } else { "printf 'line1\\nline2\\nline3\\n'" }; let mut lines: Vec<(StreamKind, String)> = Vec::new(); let res = execute_streaming(req(cmd), |kind, line| { lines.push((kind, line.to_string())); }) .await .expect("execute_streaming 应返回 Ok"); assert_eq!(res.exit_code, Some(0), "成功命令 exit_code 应为 0"); // stdout 应含三行(line1/line2/line3) assert!(res.stdout.contains("line1"), "stdout 应含 line1,实际: {:?}", res.stdout); assert!(res.stdout.contains("line3"), "stdout 应含 line3,实际: {:?}", res.stdout); // 回调收到的 stdout 行应含三行(过滤 stderr 干扰:Cmd 无 stderr,Sh 无 stderr) let stdout_lines: Vec<&String> = lines.iter() .filter(|(k, _)| *k == StreamKind::Stdout) .map(|(_, l)| l) .collect(); assert!( stdout_lines.iter().any(|l| l.contains("line1")), "回调应收到含 line1 的 stdout 行,实际: {:?}", stdout_lines ); assert!( stdout_lines.iter().any(|l| l.contains("line3")), "回调应收到含 line3 的 stdout 行,实际: {:?}", stdout_lines ); } /// 流式:超时仍生效(timeout_secs=1 + 长睡命令,返回 Err)。 #[tokio::test] async fn streaming_timeout_returns_err() { let sleep_cmd = if cfg!(windows) { "ping -n 5 127.0.0.1 > nul".to_string() } else { "sleep 5".to_string() }; let request = ShellRequest { command: sleep_cmd, working_dir: None, env: HashMap::new(), timeout_secs: Some(1), shell_type: Some(default_shell()), }; let result = execute_streaming(request, |_, _| {}).await; assert!(result.is_err(), "超时应返回 Err,实际: {:?}", result.as_ref().err()); let msg = result.unwrap_err().to_string(); assert!( msg.contains("超时") || msg.to_lowercase().contains("timeout"), "错误信息应含超时提示,实际: {}", msg ); } /// 流式:非零退出仍返回 Ok + exit_code 非 0(对齐 execute 语义)。 #[tokio::test] async fn streaming_nonzero_exit() { let mut callbacks = 0u32; let res = execute_streaming(req("exit 1"), |_, _| { callbacks += 1; }) .await .expect("非零退出应仍返回 Ok"); assert_ne!(res.exit_code, Some(0), "exit 1 的 exit_code 应非 0"); // exit 1 无输出,回调可为 0 次(无行)——不强制断言次数,只确认无 panic let _ = callbacks; }