From db2c64d1efd0048716f491e989dc50644f29e189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E6=99=A8=20=28Leo=20Cheng=29?= Date: Mon, 6 Jul 2026 04:07:20 +0800 Subject: [PATCH] feat(starry): report EOPNOTSUPP for SIOCETHTOOL and expose /proc/pid/mountinfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit socket ioctl now answers SIOCETHTOOL with EOPNOTSUPP like a virtual NIC so psutil net_if_stats() degrades gracefully; procfs exposes /proc//mountinfo in show_mountinfo layout and lists it in the thread directory. Adds ethtool-ioctl and mountinfo C regression tests. Signed-off-by: 林晨 (Leo Cheng) Signed-off-by: 林晨 --- os/StarryOS/kernel/src/file/net.rs | 16 ++ os/StarryOS/kernel/src/pseudofs/proc.rs | 25 +++ .../syscall-test-ethtool-ioctl/CMakeLists.txt | 9 + .../syscall-test-ethtool-ioctl/src/main.c | 121 ++++++++++++ .../src/test_framework.h | 95 ++++++++++ .../syscall-test-mountinfo/CMakeLists.txt | 9 + .../system/syscall-test-mountinfo/src/main.c | 178 ++++++++++++++++++ .../src/test_framework.h | 95 ++++++++++ 8 files changed, 548 insertions(+) create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/CMakeLists.txt create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/main.c create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/test_framework.h create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/CMakeLists.txt create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/main.c create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/test_framework.h diff --git a/os/StarryOS/kernel/src/file/net.rs b/os/StarryOS/kernel/src/file/net.rs index fcb5d8779a..4737f2075c 100644 --- a/os/StarryOS/kernel/src/file/net.rs +++ b/os/StarryOS/kernel/src/file/net.rs @@ -45,6 +45,8 @@ const IFF_MULTICAST: i16 = 0x1000; const IFREQ_NAME_LEN: usize = 16; const IFREQ_DATA_OFFSET: usize = 16; const IFREQ_COMPAT_LEN: usize = 40; +// ethtool ioctl; not exported by linux-raw-sys. The value is arch-independent. +const SIOCETHTOOL: u32 = 0x8946; const IFCONF_LEN_OFFSET: usize = 0; const IFCONF_BUF_OFFSET: usize = 8; @@ -345,6 +347,20 @@ impl FileLike for Socket { let idx = read_ifreq_interface(arg)?.id.get() as i32; write_ifreq_data(arg, &idx.to_ne_bytes())?; } + // Link speed/duplex query. No PHY is emulated, so report "not supported" the way a + // virtual NIC (loopback, tun/tap) does. Tools like psutil's net_if_stats() treat + // EOPNOTSUPP as "no ethtool" and degrade gracefully; any other errno makes them abort + // the whole interface-status probe. Resolve the interface first so an unknown name + // yields ENODEV, then fault on a bad ifr_data pointer, keeping Linux's error priority + // (ENODEV, then EFAULT, then EOPNOTSUPP) and parity with the sibling SIOC*IF* arms. + SIOCETHTOOL => { + read_ifreq_interface(arg)?; + let data_ptr = usize::from_ne_bytes(read_user_bytes::<8>( + (arg + IFREQ_DATA_OFFSET) as *const u8, + )?); + read_user_bytes::<4>(data_ptr as *const u8)?; + return Err(AxError::OperationNotSupported); + } _ => { if super::wext::is_wext_ioctl(cmd) { return super::wext::handle(cmd, arg); diff --git a/os/StarryOS/kernel/src/pseudofs/proc.rs b/os/StarryOS/kernel/src/pseudofs/proc.rs index 1aa7e40251..31efb3dc53 100644 --- a/os/StarryOS/kernel/src/pseudofs/proc.rs +++ b/os/StarryOS/kernel/src/pseudofs/proc.rs @@ -408,6 +408,29 @@ fn render_mounts() -> String { buf } +fn render_mountinfo() -> String { + // /proc//mountinfo (Linux fs/proc_namespace.c show_mountinfo layout): + // id parent major:minor root mount_point options [optional-fields] - fstype source super_opts + // Same mount set as render_mounts(): the root fs type is read live; the pseudo mounts are the + // fixed boot set. No optional propagation fields are emitted, so the "-" separator immediately + // precedes the fs type. Tools such as node_exporter's filesystem collector and findmnt read + // this file (in preference to /proc/mounts) to discover mount points before statfs(). + let root_fstype = { + let ctx = FS_CONTEXT.lock(); + ctx.root_dir().filesystem().name().to_string() + }; + let mut buf = format!("21 20 {VIRTBLK_MAJOR}:0 / / rw,relatime - {root_fstype} /dev/vda rw\n"); + buf.push_str("22 21 0:5 / /dev rw,nosuid,relatime - devtmpfs devtmpfs rw\n"); + buf.push_str("23 22 0:16 / /dev/shm rw,nosuid,nodev - tmpfs tmpfs rw\n"); + buf.push_str("24 21 0:17 / /tmp rw,nosuid,nodev - tmpfs tmpfs rw\n"); + buf.push_str("25 21 0:18 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw\n"); + buf.push_str("26 21 0:19 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sysfs rw\n"); + buf.push_str( + "27 26 0:20 / /sys/kernel/debug rw,nosuid,nodev,noexec,relatime - debugfs debugfs rw\n", + ); + buf +} + pub fn new_procfs() -> Filesystem { SimpleFs::new_with("proc".into(), 0x9fa0, builder) } @@ -1009,6 +1032,7 @@ impl SimpleDirOps for ThreadDir { "mem", "auxv", "mounts", + "mountinfo", "cmdline", "comm", "exe", @@ -1104,6 +1128,7 @@ impl SimpleDirOps for ThreadDir { .into(), "auxv" => SimpleFile::new_regular(fs, move || Ok(render_thread_auxv(&task))).into(), "mounts" => SimpleFile::new_regular(fs, move || Ok(render_mounts())).into(), + "mountinfo" => SimpleFile::new_regular(fs, move || Ok(render_mountinfo())).into(), "cmdline" => SimpleFile::new_regular(fs, move || { let cmdline = task.as_thread().proc_data.cmdline.read(); let mut buf = Vec::new(); diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/CMakeLists.txt new file mode 100644 index 0000000000..48d1717eb5 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ethtool-ioctl C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-ethtool-ioctl src/main.c) +target_include_directories(test-ethtool-ioctl PRIVATE src) +target_compile_options(test-ethtool-ioctl PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-ethtool-ioctl RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/main.c new file mode 100644 index 0000000000..0fe100ea71 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/main.c @@ -0,0 +1,121 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include + +/* + * SIOCETHTOOL ioctl 回归测试。 + * + * 触发背景 (为什么写这个测例): + * glances 的 NETWORK 区块通过 psutil 的 net_if_stats() 采集每个接口的状态, + * 其中 speed/duplex 用 SIOCETHTOOL (ETHTOOL_GSET) 查询 PHY 链路。starry 没有 + * 模拟 PHY, 此前 SIOCETHTOOL 落到 socket ioctl 的默认分支返回 ENOTTY。psutil + * 只把 EOPNOTSUPP 当作"无 ethtool"优雅降级, 遇到任何其它 errno 会 abort 整个 + * interface-status 探测 -> glances NETWORK 区块渲染崩溃。内核现在像虚拟网卡 + * (loopback / tun/tap) 一样对 SIOCETHTOOL 返回 EOPNOTSUPP。 + * + * Linux 参照 (fresh host, kernel 6.6): + * ioctl(AF_INET sock, SIOCETHTOOL, {ifr_name="lo"}) => -1 errno=EOPNOTSUPP(95); + * loopback 无 ethtool_ops, dev_ethtool() 返回 -EOPNOTSUPP。 + * + * 断言: + * 1. AF_INET SOCK_DGRAM 套接字创建成功。 + * 2. 对 "lo" 发 SIOCETHTOOL 返回 -1 (查询被拒)。 + * 3. errno == EOPNOTSUPP (优雅"无 ethtool"; 修复前是 ENOTTY, 会让 psutil abort)。 + */ + +/* linux-raw-sys / 常量, arch 无关; 避免依赖 。*/ +#ifndef SIOCETHTOOL +#define SIOCETHTOOL 0x8946 +#endif +#define ETHTOOL_GSET 0x00000001u + +int main(void) +{ + TEST_START("SIOCETHTOOL returns EOPNOTSUPP on virtual NIC"); + char msg[160]; + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + CHECK(fd >= 0, "socket(AF_INET, SOCK_DGRAM) 创建成功"); + if (fd < 0) { + TEST_DONE(3); + } + + /* + * ifr_data 指向一个 ethtool 命令字 (ETHTOOL_GSET)。内核在读取 ifr_data 之前 + * 就返回 EOPNOTSUPP, 但仍按 psutil 的真实调用形态填好请求。 + */ + unsigned int ethcmd = ETHTOOL_GSET; + struct ifreq ifr; + memset(&ifr, 0, sizeof ifr); + strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1); + ifr.ifr_data = (void *)ðcmd; + + errno = 0; + int ret = ioctl(fd, SIOCETHTOOL, &ifr); + int saved_errno = errno; + close(fd); + + snprintf(msg, sizeof msg, "SIOCETHTOOL 查询被拒 ret == -1 (实际 %d)", ret); + CHECK(ret == -1, msg); + + snprintf(msg, sizeof msg, + "errno == EOPNOTSUPP(%d) 优雅降级 (实际 %d %s; 修复前为 ENOTTY)", + EOPNOTSUPP, saved_errno, strerror(saved_errno)); + CHECK(saved_errno == EOPNOTSUPP, msg); + + /* + * 未知接口名: 内核应先解析接口再判 ethtool, 因此返回 ENODEV 而非无条件 + * EOPNOTSUPP。固定与 Linux 一致的错误优先级, 也与本分支其它 SIOC*IF* 一致。 + */ + int fd2 = socket(AF_INET, SOCK_DGRAM, 0); + CHECK(fd2 >= 0, "socket(AF_INET, SOCK_DGRAM) 二次创建成功"); + + struct ifreq bad; + memset(&bad, 0, sizeof bad); + strncpy(bad.ifr_name, "nonexist99", IFNAMSIZ - 1); + bad.ifr_data = (void *)ðcmd; + + errno = 0; + int bad_ret = ioctl(fd2, SIOCETHTOOL, &bad); + int bad_errno = errno; + close(fd2); + + snprintf(msg, sizeof msg, "SIOCETHTOOL 未知接口被拒 ret == -1 (实际 %d)", bad_ret); + CHECK(bad_ret == -1, msg); + + snprintf(msg, sizeof msg, + "errno == ENODEV(%d) 接口校验先于 EOPNOTSUPP (实际 %d %s)", ENODEV, + bad_errno, strerror(bad_errno)); + CHECK(bad_errno == ENODEV, msg); + + /* + * 合法接口但 ifr_data 是坏指针: 内核读取 ethtool 命令字时先 fault, 返回 + * EFAULT 而非 EOPNOTSUPP。错误优先级为 ENODEV -> EFAULT -> EOPNOTSUPP。 + */ + int fd3 = socket(AF_INET, SOCK_DGRAM, 0); + CHECK(fd3 >= 0, "socket(AF_INET, SOCK_DGRAM) 三次创建成功"); + + struct ifreq faulty; + memset(&faulty, 0, sizeof faulty); + strncpy(faulty.ifr_name, "lo", IFNAMSIZ - 1); + faulty.ifr_data = (void *)1; + + errno = 0; + int fault_ret = ioctl(fd3, SIOCETHTOOL, &faulty); + int fault_errno = errno; + close(fd3); + + snprintf(msg, sizeof msg, "SIOCETHTOOL 坏 ifr_data 被拒 ret == -1 (实际 %d)", fault_ret); + CHECK(fault_ret == -1, msg); + + snprintf(msg, sizeof msg, + "errno == EFAULT(%d) 坏指针先于 EOPNOTSUPP (实际 %d %s)", EFAULT, + fault_errno, strerror(fault_errno)); + CHECK(fault_errno == EFAULT, msg); + + // 9 = 本文件 CHECK 总数。 + TEST_DONE(9); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/test_framework.h new file mode 100644 index 0000000000..faf42d13de --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-ethtool-ioctl/src/test_framework.h @@ -0,0 +1,95 @@ +#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") + +/* + * 结束测试。expected = 本文件声明的 CHECK 总数。 + * 只有恰好跑满 expected 个检查且零失败才返回 0(成功); + * 零检查 / 少跑(早退/异常) / 有失败 一律返回 1(失败), + * 堵死"只跑部分模块也算过"的假阳性。 + */ +#define TEST_DONE(expected) \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail, %d/%d checks run\n", \ + __pass, __fail, __pass + __fail, (expected)); \ + if ((__pass + __fail) != (expected)) \ + printf(" FAIL | %s:%d | ran %d check(s), expected %d\n", \ + __FILE__, __LINE__, __pass + __fail, (expected)); \ + printf("================================================\n\n"); \ + return ((__fail > 0) || ((__pass + __fail) != (expected))) ? 1 : 0 diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/CMakeLists.txt new file mode 100644 index 0000000000..e6ea6dd14f --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-mountinfo C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-mountinfo src/main.c) +target_include_directories(test-mountinfo PRIVATE src) +target_compile_options(test-mountinfo PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-mountinfo RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/main.c new file mode 100644 index 0000000000..9e8af757a7 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/main.c @@ -0,0 +1,178 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include + +/* + * /proc//mountinfo 回归测试。 + * + * 触发背景 (为什么写这个测例): + * glances / psutil 优先解析 /proc/pid/mountinfo (而非 /proc/mounts) 来发现挂载点, + * 再对每个挂载点 statfs() 填 FILE SYS 区块。starry 此前没有 /proc/pid/mountinfo, + * glances FILE SYS 区块无法渲染。内核现按 Linux fs/proc_namespace.c show_mountinfo + * 布局导出该文件。 + * + * Linux show_mountinfo 每行布局 (man 5 proc §/proc/[pid]/mountinfo): + * id parent major:minor root mount_point mount_options [optional...] - fstype source super_opts + * "-" 是可选字段的终止符, 其后紧跟 fstype / mount source / super options。 + * + * 断言: + * 1. /proc/self/mountinfo 可打开 (修复前不存在 -> 打不开)。 + * 2. 文件非空。 + * 3-13. 根挂载点 "/" 的行满足 show_mountinfo 布局的每个字段约束。 + * 14. 伪文件系统挂载可见 (/proc, fstype proc)。 + */ + +#define MOUNTINFO_PATH "/proc/self/mountinfo" + +typedef struct { + int have_line; /* 找到 mount_point == "/" 的行 */ + int ntok_before; /* 分隔符 "-" 之前的令牌数 */ + int have_sep; + char id[32]; + char parent[32]; + char majmin[32]; + char root[64]; + char opts[160]; + char fstype[48]; + char source[160]; + char super_opts[160]; +} rootline_t; + +static int all_digits(const char *s) +{ + if (!s || !*s) + return 0; + for (; *s; s++) + if (!isdigit((unsigned char)*s)) + return 0; + return 1; +} + +/* 形如 "N:N", 冒号两侧均为非空数字串。*/ +static int valid_majmin(const char *s) +{ + const char *colon = strchr(s, ':'); + if (!colon || colon == s || colon[1] == '\0') + return 0; + for (const char *p = s; p < colon; p++) + if (!isdigit((unsigned char)*p)) + return 0; + for (const char *p = colon + 1; *p; p++) + if (!isdigit((unsigned char)*p)) + return 0; + return 1; +} + +static void copy_tok(char *dst, size_t cap, const char *src) +{ + if (!src) { + dst[0] = '\0'; + return; + } + snprintf(dst, cap, "%s", src); +} + +int main(void) +{ + TEST_START("/proc/self/mountinfo show_mountinfo layout"); + + rootline_t rl; + memset(&rl, 0, sizeof rl); + int any_line = 0; + int proc_found = 0; + + FILE *f = fopen(MOUNTINFO_PATH, "r"); + if (f) { + char line[1024]; + while (fgets(line, sizeof line, f)) { + char *toks[64]; + int n = 0; + for (char *p = strtok(line, " \t\n"); p && n < 64; + p = strtok(NULL, " \t\n")) + toks[n++] = p; + if (n == 0) + continue; + any_line = 1; + + /* 定位可选字段终止符 "-"。*/ + int sep = -1; + for (int i = 0; i < n; i++) + if (strcmp(toks[i], "-") == 0) { + sep = i; + break; + } + + const char *mp = n > 4 ? toks[4] : NULL; + + /* /proc 伪文件系统: 挂载点 /proc 且分隔符后 fstype == proc。*/ + if (mp && strcmp(mp, "/proc") == 0 && sep >= 0 && sep + 1 < n && + strcmp(toks[sep + 1], "proc") == 0) + proc_found = 1; + + if (mp && strcmp(mp, "/") == 0 && !rl.have_line) { + rl.have_line = 1; + rl.ntok_before = sep >= 0 ? sep : n; + rl.have_sep = sep >= 0; + if (n > 0) + copy_tok(rl.id, sizeof rl.id, toks[0]); + if (n > 1) + copy_tok(rl.parent, sizeof rl.parent, toks[1]); + if (n > 2) + copy_tok(rl.majmin, sizeof rl.majmin, toks[2]); + if (n > 3) + copy_tok(rl.root, sizeof rl.root, toks[3]); + if (n > 5) + copy_tok(rl.opts, sizeof rl.opts, toks[5]); + if (sep >= 0 && sep + 1 < n) + copy_tok(rl.fstype, sizeof rl.fstype, toks[sep + 1]); + if (sep >= 0 && sep + 2 < n) + copy_tok(rl.source, sizeof rl.source, toks[sep + 2]); + if (sep >= 0 && sep + 3 < n) + copy_tok(rl.super_opts, sizeof rl.super_opts, toks[sep + 3]); + } + } + fclose(f); + } + + char msg[256]; + + CHECK(f != NULL, MOUNTINFO_PATH " 可打开 (修复前不存在)"); + CHECK(any_line, "mountinfo 至少一行"); + CHECK(rl.have_line, "含根挂载点 \"/\" 的行"); + + snprintf(msg, sizeof msg, + "根行分隔符前 >=6 字段 id/parent/maj:min/root/mp/opts (实际 %d)", + rl.ntok_before); + CHECK(rl.ntok_before >= 6, msg); + + snprintf(msg, sizeof msg, "字段1 mount id 为数字 (\"%s\")", rl.id); + CHECK(all_digits(rl.id), msg); + + snprintf(msg, sizeof msg, "字段2 parent id 为数字 (\"%s\")", rl.parent); + CHECK(all_digits(rl.parent), msg); + + snprintf(msg, sizeof msg, "字段3 major:minor 形如 N:N (\"%s\")", rl.majmin); + CHECK(valid_majmin(rl.majmin), msg); + + snprintf(msg, sizeof msg, "字段4 fs 内 root == \"/\" (\"%s\")", rl.root); + CHECK(strcmp(rl.root, "/") == 0, msg); + + snprintf(msg, sizeof msg, "字段6 mount options 含 rw (\"%s\")", rl.opts); + CHECK(strstr(rl.opts, "rw") != NULL, msg); + + CHECK(rl.have_sep, "根行含 \"-\" 可选字段分隔符"); + + snprintf(msg, sizeof msg, "分隔符后 fstype 非空 (\"%s\")", rl.fstype); + CHECK(rl.fstype[0] != '\0', msg); + + snprintf(msg, sizeof msg, "分隔符后 mount source 非空 (\"%s\")", rl.source); + CHECK(rl.source[0] != '\0', msg); + + snprintf(msg, sizeof msg, "分隔符后 super options 非空 (\"%s\")", rl.super_opts); + CHECK(rl.super_opts[0] != '\0', msg); + + CHECK(proc_found, "含 /proc (fstype proc) 伪文件系统挂载行"); + + // 14 = 本文件 CHECK 总数。 + TEST_DONE(14); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/test_framework.h new file mode 100644 index 0000000000..faf42d13de --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-mountinfo/src/test_framework.h @@ -0,0 +1,95 @@ +#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") + +/* + * 结束测试。expected = 本文件声明的 CHECK 总数。 + * 只有恰好跑满 expected 个检查且零失败才返回 0(成功); + * 零检查 / 少跑(早退/异常) / 有失败 一律返回 1(失败), + * 堵死"只跑部分模块也算过"的假阳性。 + */ +#define TEST_DONE(expected) \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail, %d/%d checks run\n", \ + __pass, __fail, __pass + __fail, (expected)); \ + if ((__pass + __fail) != (expected)) \ + printf(" FAIL | %s:%d | ran %d check(s), expected %d\n", \ + __FILE__, __LINE__, __pass + __fail, (expected)); \ + printf("================================================\n\n"); \ + return ((__fail > 0) || ((__pass + __fail) != (expected))) ? 1 : 0