编写C语言测试程序:无需重启被测程序测试多语言数独求解器性能
这是个很棒的需求——确实,像Java这类启动开销大的语言,反复重启会严重影响性能测试的公平性。我来分享一个用C实现的持久化被测程序的测试框架思路和代码示例,完美适配你的数独求解器测试场景:
核心设计思路
我们的目标是只启动一次被测程序,通过双向管道和它建立持续通信:测试程序批量发送数独谜题,被测程序依次返回求解结果,全程保持被测进程运行,彻底规避启动开销。同时要精准统计每个谜题的求解耗时,并验证结果的正确性。
具体实现步骤与代码示例
1. 建立双向进程通信通道
用C的pipe()创建两个管道:一个负责向被测程序发送输入(数独谜题),另一个负责接收被测程序的输出(求解结果)。通过fork()+exec()启动被测程序,并将它的标准输入/输出重定向到管道。
2. 批量测试流程
- 预加载所有数独谜题到内存(比如从文件读取,确保测试过程无IO干扰)
- 启动被测程序并建立通信
- 遍历每个谜题:
- 记录求解开始时间
- 发送谜题到被测程序
- 等待并接收求解结果
- 记录结束时间,计算耗时
- 验证解的合法性
- 测试完成后,终止被测程序并回收进程
3. 核心代码片段
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/time.h> #include <string.h> #include <signal.h> #define BUFFER_SIZE 1024 // 数独验证函数:检查解是否符合原始谜题规则 int sudoku_validate(const char *puzzle, const char *solution) { // 基础长度检查(标准数独是81字符) if (strlen(puzzle) != 81 || strlen(solution) != 81) return 0; // 验证原始已知数是否匹配 for (int i = 0; i < 81; i++) { if (puzzle[i] != '.' && puzzle[i] != solution[i]) { return 0; } } // 这里可以补充完整的数独合法性检查(每行/列/3x3宫无重复) // 省略具体实现,你可以根据数独规则自行编写 return 1; } int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <被测程序命令>\n", argv[0]); exit(EXIT_FAILURE); } int to_child[2], from_child[2]; pid_t pid; char buffer[BUFFER_SIZE]; struct timeval start, end; double elapsed; // 创建双向管道 if (pipe(to_child) == -1 || pipe(from_child) == -1) { perror("Failed to create pipes"); exit(EXIT_FAILURE); } // 分叉进程启动被测程序 pid = fork(); if (pid == -1) { perror("Failed to fork process"); exit(EXIT_FAILURE); } if (pid == 0) { // 子进程:重定向标准IO到管道 close(to_child[1]); // 关闭不需要的写端 close(from_child[0]); // 关闭不需要的读端 dup2(to_child[0], STDIN_FILENO); // 绑定stdin到输入管道 dup2(from_child[1], STDOUT_FILENO); // 绑定stdout到输出管道 // 启动被测程序(支持任意可执行程序,比如java SudokuSolver) execvp(argv[1], &argv[1]); perror("Failed to execute target program"); exit(EXIT_FAILURE); } // 父进程(测试程序) close(to_child[0]); // 关闭不需要的读端 close(from_child[1]); // 关闭不需要的写端 // 示例测试用例(实际可以从文件读取) char *puzzles[] = { "53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79", "....9..5.1.....3.5.8....7.9...4..8.6.3..2...1.7....6.3.....1.2..8....4..6......." }; int num_puzzles = sizeof(puzzles)/sizeof(puzzles[0]); for (int i = 0; i < num_puzzles; i++) { // 记录求解开始时间 gettimeofday(&start, NULL); // 发送谜题(末尾加换行符作为被测程序的输入结束标记) write(to_child[1], puzzles[i], strlen(puzzles[i])); write(to_child[1], "\n", 1); // 读取求解结果 ssize_t bytes_read = read(from_child[0], buffer, BUFFER_SIZE-1); if (bytes_read <= 0) { fprintf(stderr, "Puzzle %d: Failed to receive solution\n", i+1); break; } buffer[bytes_read] = '\0'; // 记录结束时间并计算耗时 gettimeofday(&end, NULL); elapsed = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec)/1e6; // 验证结果并输出统计 if (sudoku_validate(puzzles[i], buffer)) { printf("Puzzle %d: Solved in %.4f seconds\n", i+1, elapsed); } else { printf("Puzzle %d: Invalid solution\n", i+1); } } // 终止被测程序并回收进程 kill(pid, SIGTERM); waitpid(pid, NULL, 0); // 清理资源 close(to_child[1]); close(from_child[0]); return EXIT_SUCCESS; }
关键注意事项
- 输出缓冲问题:很多语言的标准输出默认是行缓冲或全缓冲,被测程序必须在输出结果后立即刷新输出(比如Java用
System.out.flush(),Python用print(..., flush=True)),否则测试程序会一直阻塞等待输入。如果无法修改被测程序,可以考虑在测试程序中设置管道为非阻塞模式(用fcntl()),但处理逻辑会更复杂。 - 超时机制:如果某个谜题求解时间过长,建议用
select()监听管道读端并设置超时时间,避免测试程序卡住。 - 时间统计精度:如果需要更精准的计时,推荐使用
clock_gettime(CLOCK_MONOTONIC, &ts),它不受系统时间调整的影响,比gettimeofday更适合测量程序运行时间。 - 跨语言兼容性:只要被测程序支持从标准输入读取谜题、向标准输出输出结果,不管是Java、Python、C++还是其他语言实现的求解器,都能完美适配这个测试框架。
内容的提问来源于stack exchange,提问作者EmptyArsenal
相关产品推荐
相关产品推荐

