第十四章:合规测试
第十四章:合规测试
了解 POSIX 合规测试体系:测试套件、兼容性检查、自定义测试框架。
14.1 POSIX 合规测试概述
14.1.1 为什么需要合规测试
| 目的 | 说明 |
|---|---|
| 验证标准兼容性 | 确保代码在不同 POSIX 系统上行为一致 |
| 发现实现差异 | 不同 libc/内核可能有不同的边界行为 |
| 回归测试 | 修改代码后确认没有破坏标准行为 |
| 文档化行为 | 测试用例就是行为文档 |
14.1.2 主要合规测试套件
| 测试套件 | 维护者 | 覆盖范围 | 网址 |
|---|---|---|---|
| LTP (Linux Test Project) | SUSE/Red Hat/IBM | Linux 内核和系统调用 | https://github.com/linux-test-project/ltp |
| POSIX Test Suite | Austin Group | POSIX.1 接口 | https://github.com/mariotoffia/PosixTestSuite |
| glibc testsuite | GNU | glibc 函数实现 | glibc 源码 |
| Open POSIX Test Suite | — | POSIX 接口(历史项目) | — |
14.2 LTP (Linux Test Project)
14.2.1 安装与运行
# Ubuntu/Debian 安装
sudo apt install ltp
# 或从源码编译
git clone https://github.com/linux-test-project/ltp.git
cd ltp
make autotools
./configure
make -j$(nproc)
sudo make install
# 运行所有合规测试
cd /opt/ltp
sudo ./runltp
# 运行特定 POSIX 接口测试
sudo ./runltp -f syscalls
sudo ./runltp -f pty
sudo ./runltp -f timers
# 运行单个测试用例
./testcases/bin/fork01
./testcases/bin/pipe01
14.2.2 LTP 测试结果解读
<<<test_output>>>
tst_test.c:1365: INFO: Timeout per run is 0h 05m 00s
fork01 0 TINFO : Test fork01
fork01 1 TPASS : fork() works correctly ← 测试通过
fork01 0 TINFO : Cleaning up
<<<execution_status>>>
initiation_status="ok"
duration=0 termination_type=exited termination_id=0 corefile=no
| 结果 | 含义 |
|---|---|
TPASS | 测试通过 |
TFAIL | 测试失败 |
TBROK | 测试中断(环境问题) |
TCONF | 不适用(功能未配置) |
TWARN | 警告 |
14.3 自建 POSIX 合规测试框架
14.3.1 轻量级测试框架
/*
* posix_test.h - 轻量级 POSIX 合规测试框架
*/
#ifndef POSIX_TEST_H
#define POSIX_TEST_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static int _test_pass = 0;
static int _test_fail = 0;
static int _test_total = 0;
#define TEST_BEGIN(name) \
do { \
printf("[TEST] %s\n", name); \
} while (0)
#define TEST_END() \
do { \
printf(" 结果: %d 通过, %d 失败 (共 %d)\n\n", \
_test_pass, _test_fail, _test_total); \
} while (0)
#define ASSERT(expr) \
do { \
_test_total++; \
if (!(expr)) { \
printf(" FAIL [%s:%d] %s\n", __FILE__, __LINE__, #expr); \
_test_fail++; \
} else { \
printf(" PASS [%s:%d] %s\n", __FILE__, __LINE__, #expr); \
_test_pass++; \
} \
} while (0)
#define ASSERT_EQ(a, b) \
do { \
_test_total++; \
long long _a = (long long)(a); \
long long _b = (long long)(b); \
if (_a != _b) { \
printf(" FAIL [%s:%d] %s == %s (%lld != %lld)\n", \
__FILE__, __LINE__, #a, #b, _a, _b); \
_test_fail++; \
} else { \
printf(" PASS [%s:%d] %s == %s\n", __FILE__, __LINE__, #a, #b); \
_test_pass++; \
} \
} while (0)
#define ASSERT_STR_EQ(a, b) \
do { \
_test_total++; \
if (strcmp((a), (b)) != 0) { \
printf(" FAIL [%s:%d] \"%s\" != \"%s\"\n", \
__FILE__, __LINE__, (a), (b)); \
_test_fail++; \
} else { \
printf(" PASS [%s:%d] strings equal\n", __FILE__, __LINE__); \
_test_pass++; \
} \
} while (0)
#define ASSERT_ERRNO(expected) \
do { \
_test_total++; \
if (errno != (expected)) { \
printf(" FAIL [%s:%d] errno=%d, expected=%d (%s)\n", \
__FILE__, __LINE__, errno, (expected), strerror(errno)); \
_test_fail++; \
} else { \
printf(" PASS [%s:%d] errno=%d\n", __FILE__, __LINE__, errno); \
_test_pass++; \
} \
} while (0)
#define TEST_SUMMARY() \
do { \
printf("=== 测试汇总 ===\n"); \
printf("通过: %d, 失败: %d, 总计: %d\n", \
_test_pass, _test_fail, _test_total); \
return _test_fail > 0 ? 1 : 0; \
} while (0)
#endif /* POSIX_TEST_H */
14.3.2 使用测试框架
/*
* test_posix_basics.c - POSIX 基础接口合规测试
* 编译: gcc -Wall -o test_posix_basics test_posix_basics.c
*/
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
#include <string.h>
#include "posix_test.h"
static void test_file_operations(void)
{
TEST_BEGIN("文件操作 (open/read/write/close)");
const char *path = "/tmp/posix_test_file";
/* 测试 open + O_CREAT */
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
ASSERT(fd >= 0);
/* 测试 write */
const char *data = "POSIX test data";
ssize_t n = write(fd, data, strlen(data));
ASSERT_EQ(n, (long long)strlen(data));
close(fd);
/* 测试 read */
fd = open(path, O_RDONLY);
ASSERT(fd >= 0);
char buf[64];
n = read(fd, buf, sizeof(buf) - 1);
ASSERT_EQ(n, (long long)strlen(data));
buf[n] = '\0';
ASSERT_STR_EQ(buf, data);
close(fd);
/* 测试 stat */
struct stat st;
ASSERT_EQ(stat(path, &st), 0);
ASSERT(S_ISREG(st.st_mode));
ASSERT_EQ(st.st_size, (long long)strlen(data));
/* 测试 unlink */
ASSERT_EQ(unlink(path), 0);
/* 测试 open 不存在的文件 */
fd = open("/tmp/nonexistent_file_xyz", O_RDONLY);
ASSERT_EQ(fd, -1);
ASSERT_ERRNO(ENOENT);
TEST_END();
}
static void test_pipe(void)
{
TEST_BEGIN("管道 (pipe)");
int pipefd[2];
ASSERT_EQ(pipe(pipefd), 0);
/* 测试写入和读取 */
const char *msg = "pipe test";
ssize_t n = write(pipefd[1], msg, strlen(msg));
ASSERT_EQ(n, (long long)strlen(msg));
char buf[64];
n = read(pipefd[0], buf, sizeof(buf) - 1);
ASSERT_EQ(n, (long long)strlen(msg));
buf[n] = '\0';
ASSERT_STR_EQ(buf, msg);
/* 测试空管道非阻塞读取 */
int flags = fcntl(pipefd[0], F_GETFL);
fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK);
n = read(pipefd[0], buf, sizeof(buf));
ASSERT_EQ(n, -1);
ASSERT(errno == EAGAIN || errno == EWOULDBLOCK);
close(pipefd[0]);
close(pipefd[1]);
TEST_END();
}
static void test_fork_wait(void)
{
TEST_BEGIN("fork/wait");
pid_t pid = fork();
ASSERT(pid >= 0);
if (pid == 0) {
/* 子进程 */
_exit(42);
} else {
/* 父进程 */
int status;
pid_t ret = waitpid(pid, &status, 0);
ASSERT_EQ(ret, pid);
ASSERT(WIFEXITED(status));
ASSERT_EQ(WEXITSTATUS(status), 42);
}
TEST_END();
}
static void test_signals(void)
{
TEST_BEGIN("信号 (sigaction)");
volatile sig_atomic_t received = 0;
struct sigaction sa = {
.sa_handler = (void (*)(int))(void (*)(void))&received,
.sa_flags = 0,
};
/* 简单信号处理器 */
static volatile sig_atomic_t sig_count = 0;
sa.sa_handler = [](int s) { (void)s; sig_count++; };
sigemptyset(&sa.sa_mask);
struct sigaction old_sa;
ASSERT_EQ(sigaction(SIGUSR1, &sa, &old_sa), 0);
/* 发送信号给自己 */
ASSERT_EQ(raise(SIGUSR1), 0);
ASSERT_EQ(sig_count, 1);
/* 恢复旧处理 */
sigaction(SIGUSR1, &old_sa, NULL);
(void)received;
TEST_END();
}
static void test_time(void)
{
TEST_BEGIN("时间函数");
struct timespec ts;
ASSERT_EQ(clock_gettime(CLOCK_MONOTONIC, &ts), 0);
ASSERT(ts.tv_sec > 0);
/* 测试 clock_getres */
struct timespec res;
ASSERT_EQ(clock_getres(CLOCK_MONOTONIC, &res), 0);
ASSERT(res.tv_nsec > 0 || res.tv_sec > 0);
/* 测试 nanosleep */
struct timespec req = { .tv_sec = 0, .tv_nsec = 1000000 }; /* 1ms */
ASSERT_EQ(nanosleep(&req, NULL), 0);
TEST_END();
}
static void test_env(void)
{
TEST_BEGIN("环境变量");
/* 设置 */
ASSERT_EQ(setenv("_POSIX_TEST_VAR", "test_value", 1), 0);
/* 获取 */
char *val = getenv("_POSIX_TEST_VAR");
ASSERT(val != NULL);
ASSERT_STR_EQ(val, "test_value");
/* 删除 */
ASSERT_EQ(unsetenv("_POSIX_TEST_VAR"), 0);
val = getenv("_POSIX_TEST_VAR");
ASSERT(val == NULL);
TEST_END();
}
static void test_sysconf(void)
{
TEST_BEGIN("sysconf 系统配置");
long pagesize = sysconf(_SC_PAGESIZE);
ASSERT(pagesize > 0);
ASSERT_EQ(pagesize & (pagesize - 1), 0); /* 应为 2 的幂 */
long ncpu = sysconf(_SC_NPROCESSORS_ONLN);
ASSERT(ncpu > 0);
long open_max = sysconf(_SC_OPEN_MAX);
ASSERT(open_max > 0);
TEST_END();
}
int main(void)
{
printf("POSIX 合规测试套件\n");
printf("==================\n\n");
test_file_operations();
test_pipe();
test_fork_wait();
test_signals();
test_time();
test_env();
test_sysconf();
TEST_SUMMARY();
}
14.3.3 修复信号测试
上面的 test_signals 使用了 C++ lambda 风格,改为标准 C:
/* 正确的信号测试(标准 C) */
static volatile sig_atomic_t g_sig_count = 0;
static void test_sig_handler(int sig)
{
(void)sig;
g_sig_count++;
}
static void test_signals(void)
{
TEST_BEGIN("信号 (sigaction)");
struct sigaction sa = {
.sa_handler = test_sig_handler,
.sa_flags = 0,
};
sigemptyset(&sa.sa_mask);
struct sigaction old_sa;
ASSERT_EQ(sigaction(SIGUSR1, &sa, &old_sa), 0);
g_sig_count = 0;
ASSERT_EQ(raise(SIGUSR1), 0);
ASSERT_EQ(g_sig_count, 1);
sigaction(SIGUSR1, &old_sa, NULL);
TEST_END();
}
14.4 函数级合规测试
14.4.1 测试边界条件
/*
* test_edge_cases.c - POSIX 函数边界条件测试
* 编译: gcc -Wall -o test_edge_cases test_edge_cases.c
*/
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
static int tests_passed = 0;
static int tests_failed = 0;
#define CHECK(expr, desc) do { \
if (expr) { printf(" PASS: %s\n", desc); tests_passed++; } \
else { printf(" FAIL: %s\n", desc); tests_failed++; } \
} while(0)
static void test_pipe_limits(void)
{
printf("[pipe 限制]\n");
int fd[2];
CHECK(pipe(fd) == 0, "pipe() 创建成功");
/* 测试 PIPE_BUF 原子写入 */
long pipe_buf = fpathconf(fd[0], _PC_PIPE_BUF);
CHECK(pipe_buf > 0, "PIPE_BUF 可查询");
printf(" PIPE_BUF = %ld\n", pipe_buf);
/* 写入 PIPE_BUF 字节应该原子完成 */
char *buf = malloc(pipe_buf);
if (buf) {
memset(buf, 'A', pipe_buf);
/* 非阻塞写入 */
int flags = fcntl(fd[1], F_GETFL);
fcntl(fd[1], F_SETFL, flags | O_NONBLOCK);
ssize_t n = write(fd[1], buf, pipe_buf);
CHECK(n == pipe_buf, "PIPE_BUF 字节写入原子完成");
free(buf);
}
close(fd[0]);
close(fd[1]);
}
static void test_path_limits(void)
{
printf("\n[路径限制]\n");
long path_max = pathconf("/", _PC_PATH_MAX);
CHECK(path_max > 0, "PATH_MAX 可查询");
printf(" PATH_MAX = %ld\n", path_max);
long name_max = pathconf("/", _PC_NAME_MAX);
CHECK(name_max > 0, "NAME_MAX 可查询");
printf(" NAME_MAX = %ld\n", name_max);
}
static void test_fd_limits(void)
{
printf("\n[文件描述符限制]\n");
long open_max = sysconf(_SC_OPEN_MAX);
CHECK(open_max > 0, "OPEN_MAX 可查询");
printf(" OPEN_MAX = %ld\n", open_max);
/* 测试 dup2 */
int fd = open("/dev/null", 0);
if (fd >= 0) {
int newfd = dup2(fd, 100);
CHECK(newfd == 100, "dup2 到指定 fd 成功");
CHECK(close(newfd) == 0, "关闭 dup 的 fd");
CHECK(close(fd) == 0, "关闭原始 fd");
}
}
static void test_string_functions(void)
{
printf("\n[字符串函数]\n");
/* strlen */
CHECK(strlen("") == 0, "strlen(\"\") == 0");
CHECK(strlen("hello") == 5, "strlen(\"hello\") == 5");
/* strcmp */
CHECK(strcmp("abc", "abc") == 0, "strcmp 相等");
CHECK(strcmp("abc", "abd") < 0, "strcmp 小于");
CHECK(strcmp("abd", "abc") > 0, "strcmp 大于");
/* strncpy */
char dst[10];
strncpy(dst, "hello", sizeof(dst));
CHECK(strcmp(dst, "hello") == 0, "strncpy 正常复制");
/* snprintf */
char buf[64];
int n = snprintf(buf, sizeof(buf), "value=%d", 42);
CHECK(strcmp(buf, "value=42") == 0, "snprintf 格式化正确");
CHECK(n == 8, "snprintf 返回写入长度");
}
int main(void)
{
printf("POSIX 边界条件测试\n");
printf("==================\n\n");
test_pipe_limits();
test_path_limits();
test_fd_limits();
test_string_functions();
printf("\n=== 结果: %d 通过, %d 失败 ===\n",
tests_passed, tests_failed);
return tests_failed > 0 ? 1 : 0;
}
14.5 运行时兼容性检查
/*
* compat_check.c - 运行时 POSIX 兼容性检查
* 编译: gcc -Wall -o compat_check compat_check.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
typedef struct {
const char *name;
int sc_name;
long min_value;
} check_t;
int main(void)
{
struct utsname uts;
uname(&uts);
printf("系统: %s %s\n", uts.sysname, uts.release);
printf("架构: %s\n\n", uts.machine);
check_t checks[] = {
{"页面大小", _SC_PAGESIZE, 1},
{"最大打开文件数", _SC_OPEN_MAX, 64},
{"路径最大长度", _PC_PATH_MAX, 256},
{"文件名最大长度", _PC_NAME_MAX, 14},
{"管道缓冲区", _PC_PIPE_BUF, 512},
{"时钟滴答", _SC_CLK_TCK, 1},
{"参数最大长度", _SC_ARG_MAX, 4096},
};
int nchecks = sizeof(checks) / sizeof(checks[0]);
int issues = 0;
for (int i = 0; i < nchecks; i++) {
long val;
/* _PC_* 使用 pathconf,_SC_* 使用 sysconf */
if (checks[i].sc_name >= 0 && checks[i].sc_name < 1000)
val = sysconf(checks[i].sc_name);
else
val = pathconf("/", checks[i].sc_name);
if (val < 0) {
printf(" %-25s: 不支持\n", checks[i].name);
} else if (val < checks[i].min_value) {
printf(" %-25s: %ld (最小要求: %ld) ⚠️\n",
checks[i].name, val, checks[i].min_value);
issues++;
} else {
printf(" %-25s: %ld ✓\n", checks[i].name, val);
}
}
/* 检查 POSIX 版本 */
printf("\n");
#ifdef _POSIX_VERSION
printf(" POSIX 版本: %ldL", (long)_POSIX_VERSION);
if (_POSIX_VERSION >= 200809L)
printf(" ✓ (>= 200809L)\n");
else
printf(" ⚠️ (需要 >= 200809L)\n");
#else
printf(" POSIX 版本: 未定义 ⚠️\n");
issues++;
#endif
printf("\n兼容性检查: %s (%d 个问题)\n",
issues == 0 ? "通过" : "有问题", issues);
return issues > 0 ? 1 : 0;
}
14.6 持续集成 (CI) 中的 POSIX 测试
14.6.1 GitHub Actions 示例
# .github/workflows/posix-test.yml
name: POSIX Compliance Tests
on: [push, pull_request]
jobs:
test-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get install -y build-essential
- name: Build
run: make CC=gcc CFLAGS="-Wall -Wextra -D_POSIX_C_SOURCE=200809L"
- name: Run tests
run: make test
test-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: make CC=clang CFLAGS="-Wall -Wextra -D_POSIX_C_SOURCE=200809L"
- name: Run tests
run: make test
test-freebsd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: FreeBSD build
uses: vmactions/freebsd-vm@v1
with:
prepare: pkg install -y gmake
run: gmake test
14.6.2 测试 Makefile
CC ?= gcc
CFLAGS ?= -Wall -Wextra -D_POSIX_C_SOURCE=200809L
LDLIBS ?= -lpthread
TESTS = test_posix_basics test_edge_cases compat_check
.PHONY: all test clean
all: $(TESTS)
test: $(TESTS)
@echo "=== 运行 POSIX 合规测试 ==="
@failed=0; \
for t in $(TESTS); do \
echo "--- $$t ---"; \
if ./$$t; then \
echo "$$t: PASS"; \
else \
echo "$$t: FAIL"; \
failed=$$((failed + 1)); \
fi; \
echo; \
done; \
if [ $$failed -gt 0 ]; then \
echo "$$failed 个测试失败"; \
exit 1; \
fi; \
echo "所有测试通过"
clean:
rm -f $(TESTS) *.o
14.7 注意事项
⚠️ 测试隔离:每个测试应独立运行,不依赖其他测试的副作用。使用临时文件并在测试后清理。
⚠️ 权限问题:某些 POSIX 测试需要特定权限(如
mlock需要CAP_IPC_LOCK,信号测试需要用户权限)。在 CI 中配置适当权限。
⚠️ 容器环境:Docker 容器中某些测试可能不适用(如
CLOCK_REALTIME相关、网络测试)。使用TCONF标记跳过。
⚠️ 编译器警告:使用
-Wall -Wextra -Werror编译测试代码,确保无警告。
⚠️ 测试覆盖:重点测试边界条件和错误路径(
errno值),而非正常路径。
14.8 扩展阅读
- LTP 官网:https://linux-test-project.github.io/
- Open Group 认证测试:https://www.opengroup.org/certification
- Linux Kernel Selftests:
tools/testing/selftests/目录 - glibc 测试指南:glibc 源码中
INSTALL文件 - 《Test Driven Development》 — Kent Beck 著
14.9 本章小结
| 要点 | 说明 |
|---|---|
| LTP | Linux 内核和系统调用的标准测试套件 |
| 自建测试框架 | 使用宏定义轻量级 assert 测试 |
| 边界条件 | 重点测试返回值、errno、极端输入 |
| 运行时检查 | sysconf/pathconf 验证系统能力 |
| CI 集成 | Linux + macOS + FreeBSD 多平台自动测试 |
| 测试隔离 | 每个测试独立,使用临时文件 |