diff --git a/apps/starry/qemu/busybox/sh/busybox-tests.sh b/apps/starry/qemu/busybox/sh/busybox-tests.sh index 2f92b48961..6283bdcd27 100644 --- a/apps/starry/qemu/busybox/sh/busybox-tests.sh +++ b/apps/starry/qemu/busybox/sh/busybox-tests.sh @@ -1471,16 +1471,43 @@ else echo "FAIL_DETAIL: busybox_insmod (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# busybox_fdflush — issues the BLKFLSBUF ioctl. On a missing / non-block target +# it fails with a file/ioctl diagnostic. Require that diagnostic AND reject +# "applet not found" so an unregistered applet (which also exits non-zero with +# "fdflush: applet not found") can't pass on a bare rc!=0. bb_case_start "busybox_fdflush" _t=$({ timeout 10 sh -c 'busybox fdflush /tmp/bb_no_such_device 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -ne 0 ] && echo "$_t" | grep -qiE "No such|not found|can't open|cannot open|device|ioctl"; then +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -ne 0 ] \ + && echo "$_t" | grep -qiE "No such|can't open|cannot open|not a tty|inappropriate|ioctl" \ + && ! echo "$_t" | grep -qiF "applet not found"; then echo "PASS: busybox_fdflush"; bb_case_pass else echo "FAIL_DETAIL: busybox_fdflush (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# fdflush no operand: usage error (non-zero), not "applet not found". +bb_case_start "busybox_fdflush_noarg" +_t=$({ timeout 10 sh -c 'busybox fdflush 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -ne 0 ] && ! echo "$_t" | grep -qiF "applet not found" && echo "$_t" | grep -qiE "usage|fdflush|argument"; then + echo "PASS: busybox_fdflush_noarg"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_fdflush_noarg (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# fdflush on a real (non-floppy) device node /dev/null: opens OK, FDFLUSH ioctl +# yields an error diagnostic (tolerant: rc!=0 with a diagnostic, or rc=0). +bb_case_start "busybox_fdflush_devnull" +_t=$({ timeout 10 sh -c 'busybox fdflush /dev/null 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "applet not found" && { [ "$_rc" = 0 ] || echo "$_t" | grep -qiE "ioctl|not a tty|inappropriate|invalid|error|No such"; }; then + echo "PASS: busybox_fdflush_devnull"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_fdflush_devnull (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + bb_case_start "busybox_raidautorun" _t=$({ timeout 10 sh -c 'busybox raidautorun /tmp/bb_no_such_device 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') @@ -1491,46 +1518,165 @@ else echo "FAIL_DETAIL: busybox_raidautorun (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# busybox_killall5 — the real SIGSTOP/SIGCONT job-control killall5 relies on is +# verified safely (forked child, no effect on this suite) by the dedicated +# test-job-control-stop case. Here we smoke the applet and reject "applet not +# found" so an unregistered applet can't pass: running the real session-wide +# SIGSTOP inside this suite would risk stopping the test harness itself. bb_case_start "busybox_killall5" _t=$({ timeout 10 sh -c 'busybox killall5 -h 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && echo "$_t" | grep -qiE "Usage|killall5"; then +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && echo "$_t" | grep -qiE "Usage|killall5" && ! echo "$_t" | grep -qiF "applet not found"; then echo "PASS: busybox_killall5"; bb_case_pass else echo "FAIL_DETAIL: busybox_killall5 (rc=$_rc)"; echo "$_t"; bb_case_fail fi -# busybox_rdev — rdev outputs nothing on StarryOS (no /proc/kcore etc.) -# so we only verify: applet exists, executes without hang, returns non-timeout +# busybox_rdev — reports the device mounted at "/": stats "/", takes st_dev, +# scans /dev for a block node with matching st_rdev. With the /dev/vda root +# block node (its rdev == root st_dev) rdev prints "/dev/vda /". Require exactly +# one "/dev/ /" line so stray output can't slip through. bb_case_start "busybox_rdev" _t=$({ timeout 10 sh -c 'busybox rdev 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ]; then +_t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$(printf '%s\n' "$_t" | grep -cE '^/dev/[^[:space:]]+[[:space:]]+/$')" = 1 ]; then echo "PASS: busybox_rdev"; bb_case_pass else echo "FAIL_DETAIL: busybox_rdev (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# busybox_setlogcons — real TIOCLINUX subcmd 11 ioctl on /dev/tty0 (not just +# -h). Accept rc=0 (kernel handled it) or a "/dev/tty0" open-failure fallback +# (the applet itself works; starry may lack /dev/tty0). Reject "applet not +# found" so an unregistered applet can't pass. bb_case_start "busybox_setlogcons" -_t=$({ timeout 10 sh -c 'busybox setlogcons -h 2>&1'; echo "EXIT:$?"; } 2>&1) +_t=$({ timeout 10 sh -c 'busybox setlogcons 0 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && echo "$_t" | grep -qiE "Usage|setlogcons"; then +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "not found" \ + && { [ "$_rc" = 0 ] || echo "$_t" | grep -qF "/dev/tty0"; }; then echo "PASS: busybox_setlogcons"; bb_case_pass else echo "FAIL_DETAIL: busybox_setlogcons (rc=$_rc)"; echo "$_t"; bb_case_fail fi -# busybox_resize — outputs terminal escape sequences that corrupt EXIT: parsing -# redirect all output to /dev/null, only capture return code +# setlogcons with no positional arg: defaults to console 0 (same TIOCLINUX path). +bb_case_start "busybox_setlogcons_noarg" +_t=$({ timeout 10 sh -c 'busybox setlogcons 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "not found" && { [ "$_rc" = 0 ] || echo "$_t" | grep -qiE "/dev/tty0|console|ioctl|usage|TIOCLINUX"; }; then + echo "PASS: busybox_setlogcons_noarg"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_setlogcons_noarg (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# setlogcons with explicit console 1 exercises the N argument path. +bb_case_start "busybox_setlogcons_n1" +_t=$({ timeout 10 sh -c 'busybox setlogcons 1 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "not found" && { [ "$_rc" = 0 ] || echo "$_t" | grep -qiE "/dev/tty|console|ioctl|TIOCLINUX"; }; then + echo "PASS: busybox_setlogcons_n1"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_setlogcons_n1 (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# busybox_resize — sets the tty to raw mode (TCSETS), writes an ESC[6n cursor +# query, reads the report, then prints a `COLUMNS=N;LINES=N;export COLUMNS LINES;` +# trailer scripts can `eval`. The busybox-shell stdio console on starry does not +# yet honor the TCSETS raw-mode ioctl (it returns ENOTTY — see notes/51), so +# resize cannot complete the probe and exits non-zero before emitting the +# trailer. Accept EITHER outcome: the full trailer (the ideal, once the console +# gains TCSETS) OR a clean non-zero exit that is NOT "applet not found" (the +# current starry reality). A stub that silently exits 0 without a trailer, or a +# dropped/unregistered applet ("applet not found"), still FAILs. bb_case_start "busybox_resize" -_t=$({ timeout 10 sh -c 'busybox resize >/dev/null 2>&1'; echo "EXIT:$?"; } 2>&1) -_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ]; then +timeout 10 sh -c 'busybox resize >/tmp/bb_resize.out 2>/tmp/bb_resize.err'; _rc=$? +_rerr=$(cat /tmp/bb_resize.err 2>/dev/null) +_rtrailer=0 +if grep -qF "COLUMNS=" /tmp/bb_resize.out && grep -qF "LINES=" /tmp/bb_resize.out && grep -qF "export COLUMNS LINES" /tmp/bb_resize.out; then _rtrailer=1; fi +if [ "$_rc" -ne 124 ] && ! echo "$_rerr" | grep -qiF "applet not found" && { [ "$_rtrailer" = 1 ] || [ "$_rc" -ne 0 ]; }; then echo "PASS: busybox_resize"; bb_case_pass else - echo "FAIL_DETAIL: busybox_resize (rc=$_rc)"; echo "$_t"; bb_case_fail + echo "FAIL_DETAIL: busybox_resize (rc=$_rc trailer=$_rtrailer)"; echo "OUT:"; cat /tmp/bb_resize.out; echo "ERR: $_rerr"; bb_case_fail +fi + +# resize must be a registered applet (built in), not "applet not found". +bb_case_start "busybox_resize_registered" +_t=$({ timeout 10 sh -c 'busybox resize --help 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "applet not found"; then + echo "PASS: busybox_resize_registered"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_resize_registered (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# resize with stdin redirected from a non-tty must stay bounded (no hang) and +# must not be reported as "applet not found". +bb_case_start "busybox_resize_redir_stdin" +timeout 10 sh -c 'busybox resize /tmp/bb_resize2.out 2>&1'; _rc=$? +if [ "$_rc" -ne 124 ] && ! grep -qiF "applet not found" /tmp/bb_resize2.out 2>/dev/null; then + echo "PASS: busybox_resize_redir_stdin"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_resize_redir_stdin (rc=$_rc)"; cat /tmp/bb_resize2.out 2>/dev/null; bb_case_fail +fi + +# busybox_remove_shell — removes the named shell(s) from /etc/shells. Seed +# /etc/shells with two entries, remove one, and require the removed entry is +# gone while the survivor remains — a real edit of the file, not just --help. +bb_case_start "busybox_remove_shell" +printf '/bin/sh\n/bin/bb_dummy_shell\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell /bin/bb_dummy_shell 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') +_t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -eq 0 ] \ + && ! echo "$_t" | grep -qiF "applet not found" \ + && ! grep -qF "/bin/bb_dummy_shell" /etc/shells 2>/dev/null \ + && grep -qF "/bin/sh" /etc/shells 2>/dev/null; then + echo "PASS: busybox_remove_shell"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail +fi + +# remove-shell removes multiple shells in one invocation; survivors retained. +bb_case_start "busybox_remove_shell_multi" +printf '/bin/sh\n/bin/aaa\n/bin/bbb\n/bin/ccc\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell /bin/aaa /bin/ccc 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -eq 0 ] && ! echo "$_t" | grep -qiF "applet not found" \ + && ! grep -qF "/bin/aaa" /etc/shells && ! grep -qF "/bin/ccc" /etc/shells \ + && grep -qF "/bin/sh" /etc/shells && grep -qF "/bin/bbb" /etc/shells; then + echo "PASS: busybox_remove_shell_multi"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell_multi (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail +fi + +# remove-shell of a shell not present is a no-op success; file left intact. +bb_case_start "busybox_remove_shell_absent" +printf '/bin/sh\n/bin/zsh\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell /bin/nonexistent_xyz 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -eq 0 ] && ! echo "$_t" | grep -qiF "applet not found" \ + && grep -qF "/bin/sh" /etc/shells && grep -qF "/bin/zsh" /etc/shells; then + echo "PASS: busybox_remove_shell_absent"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell_absent (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail +fi + +# remove-shell with no operand: the per-arg removal loop has nothing to do, so +# BusyBox treats it as a no-op (rc 0) that leaves /etc/shells intact (some +# builds instead print usage with rc!=0). Accept either, but the applet must be +# registered ("applet not found" rejected) and must not corrupt the file. +bb_case_start "busybox_remove_shell_noarg" +printf '/bin/sh\n/bin/ksh\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "applet not found" \ + && grep -qF "/bin/sh" /etc/shells && grep -qF "/bin/ksh" /etc/shells; then + echo "PASS: busybox_remove_shell_noarg"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell_noarg (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail fi diff --git a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs index ade86a203f..86bb6cd023 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs @@ -83,6 +83,32 @@ impl DeviceOps for Null { } } +/// Placeholder root block device. starry has no real block-device backend for +/// the root mount; this node exists only so tools that resolve the root device +/// by scanning /dev (e.g. busybox `rdev`) can find a block node whose `rdev` +/// matches the root filesystem's `st_dev`. Real block I/O is unsupported: +/// read/write return `EIO` rather than silently succeeding, so the node never +/// masquerades as a working disk for `dd`/`blkid`/`fsck`. +struct RootBlk; + +impl DeviceOps for RootBlk { + fn read_at(&self, _buf: &mut [u8], _offset: u64) -> VfsResult { + Err(AxError::Io) + } + + fn write_at(&self, _buf: &[u8], _offset: u64) -> VfsResult { + Err(AxError::Io) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn flags(&self) -> NodeFlags { + NodeFlags::NON_CACHEABLE + } +} + struct Zero; impl DeviceOps for Zero { @@ -223,6 +249,20 @@ fn builder(fs: Arc) -> DirMaker { Arc::new(Random::new()), ), ); + // Root block device node. Its rdev must equal the root filesystem's st_dev + // so that tools resolving the root device by scanning /dev (e.g. busybox + // `rdev`, which stats "/" then looks for a block node with a matching + // st_rdev) can find it. The root mount is the first mount, so its + // `DEVICE_COUNTER` id is 1 (== `DeviceId::new(0, 1).0`). + root.add( + "vda", + Device::new( + fs.clone(), + NodeType::BlockDevice, + DeviceId::new(0, 1), + Arc::new(RootBlk), + ), + ); if ax_display::has_display() { root.add( "fb0", diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt new file mode 100644 index 0000000000..094258649c --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-rdev-vda C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-rdev-vda src/main.c) +target_include_directories(test-rdev-vda PRIVATE src) +target_compile_options(test-rdev-vda PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-rdev-vda RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c new file mode 100644 index 0000000000..00c8187ec0 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c @@ -0,0 +1,50 @@ +/* + * test-rdev-vda — root block device node /dev/vda. + * + * starry has no real block backend for the root mount, but tools that resolve + * the root device by scanning /dev (notably busybox `rdev`, which stats "/", + * takes its st_dev, then looks for a block node in /dev whose st_rdev matches) + * need such a node to exist. The kernel exposes /dev/vda as a placeholder block + * device whose rdev equals the root filesystem's st_dev. Real I/O is + * unsupported (read/write return EIO) so it never masquerades as a working disk. + * + * This is the kernel-side regression for busybox_rdev printing `/dev/vda /`. + */ + +#include "test_framework.h" + +#include +#include +#include +#include + +int main(void) +{ + TEST_START("rdev: /dev/vda root block device"); + + struct stat root_st, vda_st; + CHECK(stat("/", &root_st) == 0, "stat / (root mount)"); + + int have_vda = (stat("/dev/vda", &vda_st) == 0); + CHECK(have_vda, "stat /dev/vda (root block device node exists)"); + if (have_vda) { + CHECK(S_ISBLK(vda_st.st_mode), "/dev/vda is a block device (S_ISBLK)"); + CHECK(vda_st.st_rdev == root_st.st_dev, + "/dev/vda st_rdev == root filesystem st_dev (busybox rdev resolves \"/\" -> /dev/vda)"); + + /* RootBlk returns EIO on real I/O — it is a resolver placeholder, not a + * working disk; it must not silently succeed for dd/blkid/fsck. */ + int fd = open("/dev/vda", O_RDONLY); + CHECK(fd >= 0, "open /dev/vda O_RDONLY"); + if (fd >= 0) { + char buf[16]; + errno = 0; + ssize_t n = read(fd, buf, sizeof buf); + CHECK(n < 0 && errno == EIO, + "read /dev/vda returns EIO (placeholder, no fake disk I/O)"); + close(fd); + } + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0