如何单元测试生成子进程的函数?minishell执行器测试疑难
Minishell测试问题与解决方案
问题背景
测试用例
Test(minishell_loop, test_ls_exit) { t_minishell sheldon; ft_bzero(&sheldon, sizeof(t_minishell)); sheldon.command_line = ft_strdup("ls"); minishell_loop(&sheldon); }
minishell_loop函数
int32_t minishell_loop(t_minishell *sheldon) { if (!sheldon->command_line) sheldon->command_line = readline(PROMPT); if (!sheldon->command_line && printf("\x1B[1A" PROMPT "exit\n")) return (STOP); if (!*sheldon->command_line) { free(sheldon->command_line); return (CONTINUE); } add_history(sheldon->command_line); sheldon->tokens = lexer(sheldon->command_line, &sheldon->exit_status); if (!sheldon->tokens) return (minishell_clean(sheldon)); sheldon->ast = parser(sheldon->tokens); if (!sheldon->ast) return (minishell_clean(sheldon)); // sheldon->exit_status = executor(sheldon); return (minishell_clean(sheldon)); }
执行器核心函数(execute_simple_command)
int32_t execute_simple_command(t_command *cmd, t_minishell *shell) { pid_t pid; int32_t status; char **arguments; arguments = get_arguments(cmd); setup_redirects(cmd); free(cmd); status = execute_builtin(arguments, shell); if (status >= 0) return (status); pid = fork(); if (pid != 0) return (wait_for_child_processes(pid)); reset_signals(); execute_child_command(shell, arguments); return (0); }
错误现象
注释executor调用时测试正常运行,能打印AST;取消注释后,测试输出错误:
Command_table #1 at 0x561aa47ce490 Command #1 at 0x561aa47ce490 Argument #1: ls [ERR ] Received message identified by a PID '468695' that is not a child process.
问题解答
1. 不注释executor代码正常运行测试的方法
错误核心是wait_for_child_processes捕获了非当前shell创建的子进程,或测试环境与fork/信号逻辑冲突,可通过以下方式修复:
(1)限制waitpid仅等待当前fork的子进程
检查wait_for_child_processes实现,确保使用waitpid(pid, &status, 0)而非waitpid(-1, ...),避免捕获测试框架的其他进程:
int32_t wait_for_child_processes(pid_t target_pid) { int32_t status; // 只等待指定的子进程,而非任意子进程 waitpid(target_pid, &status, 0); return WEXITSTATUS(status); }
(2)测试模式下跳过真实外部命令执行
添加环境变量标记,在测试时mock外部命令的执行,避免真实fork:
- 修改
execute_simple_command:
#include <stdlib.h> int32_t execute_simple_command(t_command *cmd, t_minishell *shell) { pid_t pid; int32_t status; char **arguments; arguments = get_arguments(cmd); setup_redirects(cmd); free(cmd); status = execute_builtin(arguments, shell); if (status >= 0) return (status); // 测试模式下直接返回模拟状态,跳过fork if (getenv("TEST_MODE")) { free(arguments); return 0; } pid = fork(); if (pid != 0) return (wait_for_child_processes(pid)); reset_signals(); execute_child_command(shell, arguments); _exit(EXIT_FAILURE); // 确保子进程不会返回测试框架 return (0); }
- 修改测试用例,设置测试环境变量:
Test(minishell_loop, test_ls_exit) { t_minishell sheldon; setenv("TEST_MODE", "1", 1); ft_bzero(&sheldon, sizeof(t_minishell)); sheldon.command_line = ft_strdup("ls"); minishell_loop(&sheldon); unsetenv("TEST_MODE"); }
(3)修复子进程退出逻辑
确保execute_child_command执行后调用_exit而非return,防止子进程回到测试框架代码,导致PID混乱。
2. 无法直接测试时的替代方案
如果上述方法无法生效,可拆分测试粒度,或隔离外部依赖:
单元拆分测试:
- 单独测试
lexer:输入命令行字符串,验证输出的token列表是否符合预期。 - 单独测试
parser:输入预设的token列表,验证生成的AST结构正确。 - 单独测试内置命令:直接调用
execute_builtin,传入模拟的参数和shell结构。
- 单独测试
Mock系统调用:
使用测试框架(如cmocka)对fork、waitpid、execve等系统调用做mock,模拟子进程执行结果,无需真实创建进程。独立进程集成测试:
不在测试用例中直接调用minishell_loop,而是启动独立的minishell进程,通过管道重定向输入输出,发送命令并捕获结果,避免测试框架与minishell的进程逻辑冲突。
3. 输出与bash对比的实现方法
要验证minishell输出与bash一致,可通过重定向+文件对比实现:
(1)重定向输出到临时文件
#include <fcntl.h> #include <sys/wait.h> #include <unistd.h> #include <assert.h> // 辅助函数:对比两个文件内容是否一致 int file_content_equal(const char *file1, const char *file2) { // 实现文件内容对比逻辑(略) return 1; // 示例返回值,需替换为真实逻辑 } Test(minishell_output, test_ls) { t_minishell sheldon; ft_bzero(&sheldon, sizeof(t_minishell)); sheldon.command_line = ft_strdup("ls"); // 捕获minishell输出 int ms_fd = open("minishell_out", O_WRONLY | O_CREAT | O_TRUNC, 0644); int old_stdout = dup(STDOUT_FILENO); int old_stderr = dup(STDERR_FILENO); dup2(ms_fd, STDOUT_FILENO); dup2(ms_fd, STDERR_FILENO); minishell_loop(&sheldon); close(ms_fd); dup2(old_stdout, STDOUT_FILENO); dup2(old_stderr, STDERR_FILENO); close(old_stdout); close(old_stderr); // 捕获bash输出 int bash_fd = open("bash_out", O_WRONLY | O_CREAT | O_TRUNC, 0644); pid_t bash_pid = fork(); if (bash_pid == 0) { dup2(bash_fd, STDOUT_FILENO); dup2(bash_fd, STDERR_FILENO); execl("/bin/bash", "bash", "-c", "ls", NULL); _exit(EXIT_FAILURE); } waitpid(bash_pid, NULL, 0); close(bash_fd); // 对比输出 assert(file_content_equal("minishell_out", "bash_out")); // 清理临时文件 unlink("minishell_out"); unlink("bash_out"); }
(2)忽略环境相关差异
bash输出可能因环境(如颜色、文件顺序)产生差异,可:
- 对输出内容排序后再对比。
- 忽略空格、换行的细微差异,只验证核心内容(如文件名列表)。
- 禁用bash的颜色输出(执行
bash -c 'LS_COLORS= ls')。
内容的提问来源于stack exchange,提问作者Saladin Afoh
相关产品推荐
相关产品推荐

