# openruyi-autotest 开发指南 > 如何为 openruyi-autotest 贡献新的测试用例。 > > **相关文档**:[Commit & MR 规范](commit_guide.md) · [用户指南](user_guide.md) --- ## 1. 测试框架概述 openruyi-autotest 基于 [tmt (Test Management Tool)](https://tmt.readthedocs.io/) 框架,使用 [BeakerLib](https://github.com/beakerlib/beakerlib) 编写测试脚本,通过 [FMF (Flexible Metadata Format)](https://fmf.readthedocs.io/) 管理元数据。 ### 核心概念 | 概念 | 说明 | 文件 | |------|------|------| | **测试计划 (Plan)** | 定义如何发现、准备、执行和报告测试 | `plans/*.fmf` | | **测试用例 (Test)** | 具体的测试脚本 + 元数据 | `tests/**/test.sh` + `main.fmf` | | **FMF 元数据** | YAML 格式的测试描述信息 | `main.fmf` | | **BeakerLib** | Shell 测试框架,提供 rlRun/rlAssertGrep 等原语 | 脚本中 `. /usr/share/beakerlib/beakerlib.sh` | --- ## 2. 目录约定 ``` tests/ ├── main.fmf # 全局共享配置(framework, duration 等) ├── smoke/ # 冒烟测试 │ ├── main.fmf # 冒烟级共享配置 │ └── / # 分类目录 │ ├── main.fmf # 分类共享配置 │ ├── lib.sh # 分类级共享库(可选) │ └── test_smoke_/ # 测试用例目录 │ ├── main.fmf # 用例元数据 │ └── test.sh # 用例脚本 ├── functional/ # 功能测试 │ ├── main.fmf # 功能级共享配置 │ └── pkgs/ # RPM 软件包测试 │ └── / # 软件包目录 │ ├── main.fmf # 包级元数据 │ ├── lib.sh # 包级共享库(可选) │ └── test__/ # 功能测试用例 │ ├── main.fmf │ └── test.sh ├── security/ # 安全测试 ├── compatibility/ # 兼容性测试 ├── performance/ # 性能测试 ├── feature/ # 特性测试 │ ├── main.fmf # 特性级共享配置 │ └── / # 特性名称(如 gpu、network) │ ├── main.fmf # 特性级元数据 │ └── test_feature_/ # 特性测试用例(aaa 为具体描述) │ ├── main.fmf │ └── test.sh └── reliability/ # 可靠性测试 ``` --- ## 3. 添加新测试用例 ### 3.1 创建目录和文件 以在 acl 软件包下新增一个测试用例为例: ```bash # 1. 创建测试用例目录 mkdir -p tests/functional/pkgs/acl/test_acl_my_feature # 2. 创建元数据文件 cat > tests/functional/pkgs/acl/test_acl_my_feature/main.fmf << 'EOF' summary: 功能测试 - acl - 我的功能 test: ./test.sh framework: shell tag: - functional - acl duration: 2m tier: 1 path: /tests/functional/pkgs/acl/test_acl_my_feature require: - acl - coreutils - beakerlib EOF ``` > **字段说明**:`framework: shell` 声明使用 Shell 测试框架;`tag: [functional, acl]` 确保被 functional plan 发现;`duration` 可覆盖父级默认值;`require` 除被测包外还需 `coreutils`(创建文件/目录等基础操作)和 `beakerlib`(测试框架)。 ### 3.2 编写测试脚本 ```bash cat > tests/functional/pkgs/acl/test_acl_my_feature/test.sh << 'TESTEOF' #!/bin/bash # Functional test: acl - my feature # Beakerlib-based test with lifecycle management # Shared suite setup/cleanup via ../lib.sh (install once, uninstall once) . /usr/share/beakerlib/beakerlib.sh || exit 1 . "$(dirname "$0")/../lib.sh" rlJournalStart rlPhaseStartSetup "环境准备" aclSetup TmpDir=$(mktemp -d) rlRun "cd $TmpDir" 0 "进入临时测试目录" rlRun "touch testfile" 0 "创建测试文件" rlRun "mkdir testdir" 0 "创建测试目录" rlPhaseEnd rlPhaseStartTest "我的功能测试" # 功能点 1: 基本验证 rlRun "getfacl testfile" 0 "查看文件默认 ACL" # 功能点 2: 设置 ACL rlRun "setfacl -m u:root:rwx testfile" 0 "设置用户 ACL" rlRun "getfacl testfile 2>&1 | grep -q 'user:root:rwx'" 0 "确认 ACL 已设置" rlPhaseEnd rlPhaseStartCleanup "清理测试环境" rlRun "cd /" 0 "离开测试目录" if [ -n "$TmpDir" ] && [ -d "$TmpDir" ]; then rlRun "rm -rf $TmpDir" 0 "清理临时测试目录" fi # acl 软件包由 lib.sh 的引用计数机制自动管理卸载 rlPhaseEnd rlJournalPrintText rlJournalEnd TESTEOF ``` > **`rlRun` 退出码规则**:第二个参数 `0` 表示期望命令成功(退出码 0),`1` 表示期望命令失败(用于负向测试)。如果实际退出码与期望不符,rlRun 会报告 FAIL 并阻止后续 Phase 执行。 ### 3.3 BeakerLib 生命周期 每个测试脚本遵循标准的三阶段结构: | 阶段 | 函数 | 用途 | |------|------|------| | **Setup** | `rlPhaseStartSetup` | 环境准备:安装软件包、创建临时目录、调用 `*Setup()` 共享库函数 | | **Test** | `rlPhaseStartTest` | 执行测试:调用被测试命令、断言结果。可包含多个 Phase,每个测试一个功能点 | | **Cleanup** | `rlPhaseStartCleanup` | 清理环境:删除临时文件、返回原目录、共享库自动管理软件包卸载 | > **注意**:`rlPhaseStartSetup` 中调用的 `aclSetup()` 通过 `rlCleanupAppend` 向 BeakerLib 注册了清理回调,所以 Cleanup 阶段**不需要**手动卸载软件包。 ### 3.3.1 测试如何被 tmt 发现? tmt 的执行流程是:Plan 的 `discover` 阶段遍历 `tests/` 目录树,通过 FMF 的 `tag` 匹配找到所有符合条件的测试用例。 ``` tmt run plan --name /plans/functional │ ├─ [discover] plans/functional.fmf → filter: "tag:functional" │ │ │ ├─ 遍历 tests/ 树,匹配 tag 含 "functional" 的所有 main.fmf │ │ ✓ tests/functional/pkgs/acl/test_acl_getfacl_basic/main.fmf │ │ ✓ tests/functional/pkgs/acl/test_acl_setfacl_basic/main.fmf │ │ ✗ tests/smoke/archive/main.fmf (tag: smoke) │ │ ✗ tests/security/cve/main.fmf (tag: security) │ │ │ └─ 输出: 202 个测试被发现 │ ├─ [execute] 按顺序执行每个 test.sh │ └─ [report] 汇总结果 ``` **要点**: - `main.fmf` 中必须包含 `tag: [functional]`(或其他对应的计划标签)才能被对应 Plan 发现 - Plan 文件(如 `plans/functional.fmf`)通过 `filter: "tag:functional"` 声明匹配条件 - 不同 Plan 可以运行同一组测试(只需在 tag 中添加多个标签即可) - 标签名来源于测试分类名:`smoke`、`functional`、`security`、`compatibility`、`performance`、`reliability`、`feature` ### 3.4 共享库(lib.sh) 当同一个软件包下多个测试用例需要共享安装/卸载逻辑时,使用 `lib.sh`。 **设计要点**: - **`library-prefix` 注解**:文件头 `# library-prefix = acl` 声明函数前缀,所有公共函数以 `acl` 开头。 - **引用计数机制**:使用标志文件(如 `/tmp/.beakerlib_acl_suite`)跟踪有多少个测试用例正在使用该套件。第一个测试安装软件包,最后一个测试卸载。 - **`rlCleanupAppend`**:在 Setup 阶段注册 cleanup 函数,无论测试成功/失败,BeakerLib 都会在 `rlJournalEnd` 时自动调用。 - **`sudo` 处理**:项目使用 `echo | sudo -S ` 模式注入 sudo 密码,`lib.sh` 中的安装/卸载命令需要适配。 **`lib.sh` 示例**: ```bash # library-prefix = acl # # ACL suite-level shared library # Usage in each test file: # . "$(dirname "$0")/../lib.sh" # from test_acl_xxx/ subdirectories # # Then call: aclSetup in rlPhaseStartSetup # The cleanup is auto-registered via rlCleanupAppend. ACL_FLAG="/tmp/.beakerlib_acl_suite" aclSetup() { if [ ! -f "$ACL_FLAG" ]; then if ! rpm -q acl 2>/dev/null; then echo openruyi | sudo -S dnf install -y acl 2>/dev/null echo "installed=1" > "$ACL_FLAG" else echo "installed=0" > "$ACL_FLAG" fi echo "ref=1" >> "$ACL_FLAG" else local ref=$(grep "^ref=" "$ACL_FLAG" | cut -d= -f2) ref=$((ref + 1)) sed -i "s/^ref=.*/ref=$ref/" "$ACL_FLAG" fi rlCleanupAppend "aclCleanup" } aclCleanup() { [ ! -f "$ACL_FLAG" ] && return 0 local ref=$(grep "^ref=" "$ACL_FLAG" | cut -d= -f2) ref=$((ref - 1)) if [ "$ref" -le 0 ]; then grep -q "^installed=1" "$ACL_FLAG" && echo openruyi | sudo -S dnf remove -y acl 2>/dev/null || true rm -f "$ACL_FLAG" else sed -i "s/^ref=.*/ref=$ref/" "$ACL_FLAG" fi } ``` --- ## 4. FMF 元数据字段说明 ### 测试用例 (main.fmf) | 字段 | 类型 | 必填 | 说明 | |------|------|:---:|------| | `summary` | string | ✅ | 测试简短描述,格式:`测试类型 - 软件包 - 功能` | | `test` | string | ✅ | 测试脚本路径,通常 `./test.sh` | | `framework` | string | | 测试框架,`shell` 表示 Shell 脚本(继承自 `tests/main.fmf`) | | `tag` | list | ✅ | 标签列表,如 `[functional, acl]`。Plan 通过 `filter: "tag:functional"` 匹配 | | `duration` | string | ✅ | 预估执行时间,如 `2m`、`5m`。可覆盖父级默认值 | | `tier` | int | ✅ | 优先级:0=冒烟(每次提交),1=核心功能(每日),2=扩展(发布前) | | `path` | string | ✅ | 测试路径,格式 `/tests//pkgs//` | | `require` | list | ✅ | 依赖的 RPM 软件包(需含被测包 + `coreutils` + `beakerlib`) | | `contact` | string | | 测试负责人 | | `environment` | dict | | 自定义环境变量,如 `{VAR1: val1, VAR2: val2}`,注入到测试执行环境 | | `hardware-require` | dict | | 硬件需求声明(详见图 5.3),如 `{cpu: ">= 4", memory: ">= 8 GiB"}` | ### 测试计划 (plans/*.fmf) ```yaml summary: 功能测试计划 discover: how: fmf test: - /tests/functional provision: how: local prepare: how: shell script: - echo "" execute: how: tmt ``` --- ## 5. 硬件环境约束 本项目所有 Plan 使用 `how: local` 模式,通过 **FMF 元数据声明 + 环境变量注入 + 公共库自检** 实现硬件环境约束。当环境不满足声明时,测试自动 SKIP(`exit 0`),不阻塞其他用例。 ### 5.1 组件总览 | 组件 | 文件 | 作用 | |------|------|------| | 拓扑配置模板 | `topology.env.example` | 仓库级模板,提交到版本控制 | | 拓扑配置实例 | `topology.env` | 实际服务器信息,已 `.gitignore`,不提交 | | 测试用例声明 | `main.fmf` 中的 `hardware-require` | 每个用例声明自己的硬件需求 | | Plan 加载 | `plans/*.fmf` 中的 `environment-file` | 将 `topology.env` 注入为环境变量 | | 公共检查库 | `tests/lib/hw_check.sh` | 解析声明、对比环境、远程执行 | ### 5.2 拓扑配置 (`topology.env`) 从 `topology.env.example` 复制为 `topology.env`,按实际环境修改: ```bash TEST_SERVER_COUNT=2 TEST_SERVER_1_HOST=10.20.237.192 TEST_SERVER_1_PORT=12055 TEST_SERVER_1_USER=openruyi TEST_SERVER_1_PASSWORD=openruyi TEST_SERVER_2_HOST=10.20.238.100 TEST_SERVER_2_PORT=22 TEST_SERVER_2_USER=openruyi TEST_SERVER_2_PASSWORD=openruyi ``` ### 5.3 测试用例声明 (`hardware-require`) #### 支持的字段 | 字段 | 含义 | 检查方式 | 示例值 | |------|------|----------|--------| | `server` | 需要的服务器数量 | 对比 `TEST_SERVER_COUNT` | `2` | | `cpu` | 每台 CPU 核心数 | `nproc` | `">= 4"` | | `memory` | 每台可用内存 | `free -g` | `">= 8 GiB"` | | `disk` | 每台磁盘数量 | `lsblk -nd` | `">= 1"` | | `net` | 每台 UP 状态网卡数 | `ip -o link show` | `">= 1"` | 支持的比较运算符:`=` `!=` `>=` `<=` `>` `<` #### 层级继承 利用 FMF 的层级继承机制,在测试分类父级统一声明默认值,子套件无需重复: ``` tests/functional/main.fmf ← hardware-require (默认值) └─ pkgs/acl/main.fmf ← 无 hardware-require → 继承父级 │ ├─ test_acl_getfacl_basic/ → 获得默认约束 ✅ │ └─ test_acl_setfacl/ → 获得默认约束 ✅ └─ kernel/realtime/main.fmf ← cpu: ">= 16" → 覆盖 cpu └─ test_rt_latency/ → cpu>=16, 其余继承默认 ✅ ``` #### 当前默认值 所有测试分类父级 `tests/*/main.fmf` 已统一声明: ```yaml hardware-require: server: 1 cpu: ">= 4" memory: ">= 8 GiB" disk: ">= 1" net: ">= 1" ``` #### 按需覆盖 子套件只需覆盖需要提升的字段,其余自动继承: ```yaml # tests/functional/kernel/realtime/main.fmf hardware-require: cpu: ">= 16" # 覆盖父级的 ">= 4" memory: ">= 16 GiB" # 覆盖父级的 ">= 8 GiB" # server/disk/net 未写,自动继承父级默认值 ``` ### 5.4 公共库函数 (`tests/lib/hw_check.sh`) 测试脚本中引入公共库后调用: ```bash . "$(dirname "$0")/../../lib/hw_check.sh" ``` | 函数 | 用途 | |------|------| | `hwVerify [fmf]` | 综合检查所有字段,不满足则 `exit 0`(tmt 视为 skip) | | `hwServerVerify [fmf]` | 仅检查服务器数量 | | `hwCpuCheck [fmf]` | 仅检查 CPU 核心数 | | `hwMemCheck [fmf]` | 仅检查内存大小 | | `hwDiskCheck [fmf]` | 仅检查磁盘数量 | | `hwNetCheck [fmf]` | 仅检查网卡数量(UP 状态,排除 lo) | | `hwRunOnServer ` | 在指定索引的服务器上远程执行命令 | | `hwGetServerInfo ` | 获取服务器连接信息 (`host`/`port`/`user`/`password`) | ### 5.5 Plan 配置 所有 `how: local` 的 Plan 需添加 `environment-file` 以加载拓扑环境变量: ```yaml # plans/functional.fmf environment-file: - topology.env ``` 当前项目所有 Plan 文件已统一配置:`functional`、`smoke`、`security`、`performance`、`reliability`、`compatibility`、`feature`、`all`。 ### 5.6 完整用例示例 **`main.fmf`**: ```yaml summary: 功能测试 - my_pkg - 多机主备切换 test: ./test.sh tag: - functional - my_pkg duration: 5m tier: 2 hardware-require: server: 2 ``` **`test.sh`**: ```bash #!/bin/bash . /usr/share/beakerlib/beakerlib.sh || exit 1 . "$(dirname "$0")/../../lib/hw_check.sh" # ← 引入公共库 rlJournalStart rlPhaseStartSetup "环境准备" rlRun "dnf install -y my-pkg" 0 "安装 my-pkg" rlPhaseEnd rlPhaseStartTest "主备切换测试" # ⚠️ hwVerify 必须放在 Test Phase 第一行 # 放在 Setup 中会导致 Cleanup 被跳过(exit 0 绕过 rlJournalEnd) hwVerify # 本机启动主服务 rlRun "my-server --start --role=master" 0 "启动主节点" # server-2 启动备服务 hwRunOnServer 2 "my-server --start --role=standby" # 从 server-2 连接 server-1 验证 local s1_host=$(hwGetServerInfo 1 host) hwRunOnServer 2 "my-client --connect $s1_host --check-status" rlPhaseEnd rlPhaseStartCleanup "清理环境" rlRun "my-server --stop" 0 "停止主节点" hwRunOnServer 2 "my-server --stop" rlPhaseEnd rlJournalPrintText rlJournalEnd ``` ### 5.7 工作机制 ``` tmt run plan --name /plans/functional │ ├─ environment-file 加载 topology.env → 环境变量就绪 │ ├─ discover: 发现所有包含 tag:functional 的测试 │ ├─ execute: │ ├─ test_acl_basic: 继承默认 hardware-require → hwVerify() 自动检查 │ ├─ test_multi_host: │ │ ├─ hwVerify() │ │ │ ├─ TEST_SERVER_COUNT=1, need=2 → "SKIP: need 2 servers" │ │ │ └─ exit 0(tmt 记录为 skip) │ │ └─ (不满足时不执行后续测试逻辑) │ └─ ... │ └─ report: 汇总所有 skip 状态及原因 ``` > **设计原则**:利用 FMF 层级继承 + 环境变量注入 + 公共库自检,实现与上层 provision 插件一致的"不满足即跳过"语义,且避免在每个套件重复声明硬件需求。 --- ## 6. 命名规范 | 规则 | 示例 | |------|------| | 测试用例目录:`test_{包名}_{功能描述}` | `test_acl_getfacl_basic` | | 全小写,下划线分隔 | `test_coreutils_ls_command` | | 功能描述用英文 | `test_bash_variable_expansion` | | 简单检查包:`test_{包名}_basic_check` | `test_attr_basic_check` | | 无分组包:`test_{包名}_main` | `test_filesystem_main` | --- ## 7. 执行和验证 ### 本地验证 ```bash # 执行单个新测试用例 tmt run --all --verbose plan --name /plans/functional \ test --name /tests/functional/pkgs/acl/test_acl_my_feature \ provision --feeling-safe # 查看结果 tmt run --last report -fvvv ``` ### 验证清单 - [ ] `main.fmf` 包含所有必填字段(summary, test, tag, duration, tier, path, require) - [ ] `tag` 包含正确的计划标签(如 `functional`),确保能被 Plan 发现 - [ ] `require` 包含被测包 + `coreutils` + `beakerlib` - [ ] `test.sh` 首行 `#!/bin/bash`,sources beakerlib 和 lib.sh - [ ] 手动执行可通过 (`bash test.sh`) - [ ] 无硬编码路径,使用相对路径和 `$TmpDir` - [ ] Cleanup 阶段正确清理临时文件,共享库自动管理软件包卸载 - [ ] 如有多机需求,调用 `hwVerify` 进行环境自检(放在 Test Phase 第一行) --- ## 8. 常见问题 ### Q: test.sh 需要可执行权限吗? tmt 通过 `bash test.sh` 执行,不强制要求,但建议 `chmod +x`。 ### Q: 如何复用其他测试的安装逻辑? 使用 `lib.sh` 共享库 + 引用计数机制(参见 3.4 节)。 ### Q: 我写了 main.fmf 但 tmt 执行时提示 0 tests discovered? 1. 确认 `tag` 字段与 Plan 的 `filter` 匹配(如 `filter: "tag:functional"`) 2. 确认 `main.fmf` 文件在正确路径下 3. 运行 `tmt test ls` 查看 tmt 发现了哪些测试: ```bash cd openruyi-autotest tmt test ls | grep ``` ### Q: rlRun 执行失败但命令本身是正确的? 可能是 rlRun 的 `expect` 参数不匹配。rlRun 第二个参数是期望退出码: - `rlRun "cmd" 0`:期望命令成功(退出码 0) - `rlRun "cmd should fail" 1`:期望命令失败(退出码 1) - 如果实际退出码与期望不符,rlRun 报告失败 ### Q: hwVerify 放在 Setup 还是 Test 阶段? **必须在 Test Phase 第一行**。`hwVerify` 不满足时会 `exit 0`,如果放在 Setup 阶段,`exit 0` 会绕过 `rlJournalEnd`,导致 Cleanup 不执行、临时文件残留。 ### Q: 如何调试失败的测试? ```bash # 查看完整输出 cat /var/tmp/tmt/run-*/plans/functional/execute/data/guest/default-0/tests/.../output.txt # 手动执行脚本(可直接调试) bash tests/functional/pkgs/acl/test_acl_my_feature/test.sh ``` --- ## 9. 实战示例:开发 ACL 测试套 本节完整演示如何从零开始为一个新的软件包开发测试套,以 `acl` 为例。 ### 9.1 总体流程 ``` 需求分析 → 创建套件目录 → 编写 lib.sh → 编写 main.fmf → 编写 test.sh → 验证执行 ``` ACL 测试套共 **11 个测试用例**,覆盖 getfacl、setfacl、chacl 三大命令的所有主要功能点。 ### 9.2 第 1 步:创建套件目录结构 ```bash # 创建软件包级目录 mkdir -p tests/functional/pkgs/acl # 创建包级元数据 cat > tests/functional/pkgs/acl/main.fmf << 'EOF' summary: 功能测试 - acl 软件包功能验证 tag: - functional - acl duration: 5m tier: 1 path: /tests/functional/pkgs/acl require: - acl - coreutils - beakerlib EOF ``` ### 9.3 第 2 步:编写共享库 `lib.sh` 11 个测试用例都需要 `acl` 软件包,使用共享库 + 引用计数避免每个用例重复安装/卸载: ```bash cat > tests/functional/pkgs/acl/lib.sh << 'EOF' # library-prefix = acl # # ACL suite-level shared library # Uses flag-file + reference counting to ensure the acl package # is installed only ONCE and uninstalled only ONCE across all # test cases. ACL_FLAG="/tmp/.beakerlib_acl_suite" aclSetup() { if [ ! -f "$ACL_FLAG" ]; then if ! rpm -q acl 2>/dev/null; then echo openruyi | sudo -S dnf install -y acl 2>/dev/null echo "installed=1" > "$ACL_FLAG" rlLogInfo "已安装 acl 软件包(首次)" else echo "installed=0" > "$ACL_FLAG" rlLogInfo "acl 软件包已存在" fi echo "ref=1" >> "$ACL_FLAG" else local ref=$(grep "^ref=" "$ACL_FLAG" | cut -d= -f2) ref=$((ref + 1)) sed -i "s/^ref=.*/ref=$ref/" "$ACL_FLAG" rlLogInfo "acl 已由其他测试安装,引用计数: $ref" fi rlCleanupAppend "aclCleanup" } aclCleanup() { if [ ! -f "$ACL_FLAG" ]; then return 0 fi local ref=$(grep "^ref=" "$ACL_FLAG" | cut -d= -f2) ref=$((ref - 1)) if [ "$ref" -le 0 ]; then if grep -q "^installed=1" "$ACL_FLAG"; then echo openruyi | sudo -S dnf remove -y acl 2>/dev/null || true rlLogInfo "已卸载 acl 软件包(最后一个测试)" fi rm -f "$ACL_FLAG" else sed -i "s/^ref=.*/ref=$ref/" "$ACL_FLAG" rlLogInfo "acl 保留(还有 $ref 个测试未完成)" fi } EOF ``` ### 9.4 第 3 步:逐功能点开发测试用例 以下按功能点分别创建目录、main.fmf 和 test.sh。 #### 9.4.1 用例 1: getfacl 基本功能 ```bash mkdir -p tests/functional/pkgs/acl/test_acl_getfacl_basic ``` **`main.fmf`**: ```yaml summary: 功能测试 - acl - getfacl 基本功能 test: ./test.sh tag: - functional - acl duration: 2m tier: 1 path: /tests/functional/pkgs/acl/test_acl_getfacl_basic require: - acl - beakerlib ``` **`test.sh`** — 覆盖 7 个功能点,每个功能点至少一个 `rlRun`: ```bash #!/bin/bash # Functional test: acl - getfacl basic . /usr/share/beakerlib/beakerlib.sh || exit 1 . "$(dirname "$0")/../lib.sh" rlJournalStart rlPhaseStartSetup "环境准备" aclSetup TmpDir=$(mktemp -d) rlRun "cd $TmpDir" 0 "进入临时测试目录" rlRun "touch testfile" 0 "创建测试文件" rlRun "mkdir testdir" 0 "创建测试目录" rlPhaseEnd rlPhaseStartTest "getfacl 基本功能" # 功能点 1: 查看文件默认 ACL(含基本权限条目) rlRun "getfacl testfile 2>&1 | grep -qE \"user::|group::|other::\"" 0 \ "查看文件默认 ACL 含权限条目" # 功能点 2: 查看目录默认 ACL rlRun "getfacl testdir 2>&1 | grep -qE \"user::|group::|other::\"" 0 \ "查看目录默认 ACL 含权限条目" # 功能点 3: -a 参数只显示 access ACL rlRun "getfacl -a testfile 2>&1 | grep -qE \"user::|group::\"" 0 \ "使用 -a 参数查看 access ACL" # 功能点 4: -d 参数只显示 default ACL rlRun "getfacl -d testfile 2>&1 | grep -qE \"user::|default\"" 0 \ "使用 -d 参数查看 default ACL 含 default 条目" # 功能点 5: -c 参数不显示注释头 rlRun "getfacl -c testfile 2>&1" 0 "使用 -c 参数不显示注释头" rlAssertNotGrep "^# file:" "$(getfacl -c testfile 2>&1)" \ "-c 输出不包含注释头" # 功能点 6: -n 参数显示数字用户/组 ID rlRun "getfacl -n testfile 2>&1 | grep -qE \"[0-9]+\"" 0 \ "使用 -n 参数显示数字 ID" # 功能点 7: -t 参数使用表格输出格式 rlRun "getfacl -t testfile 2>&1 | grep -qE \"[r-][w-][x-]\"" 0 \ "使用 -t 参数表格输出含权限位" rlPhaseEnd rlPhaseStartCleanup "清理测试环境" rlRun "cd /" 0 "离开测试目录" if [ -n "$TmpDir" ] && [ -d "$TmpDir" ]; then rlRun "rm -rf $TmpDir" 0 "清理临时测试目录" fi rlPhaseEnd rlJournalPrintText rlJournalEnd ``` #### 9.4.2 用例 2: setfacl 基本功能 ```bash mkdir -p tests/functional/pkgs/acl/test_acl_setfacl_basic ``` **`test.sh`** — 侧重 setfacl 的基础增删改查: ```bash #!/bin/bash # Functional test: acl - setfacl basic . /usr/share/beakerlib/beakerlib.sh || exit 1 . "$(dirname "$0")/../lib.sh" rlJournalStart rlPhaseStartSetup "环境准备" aclSetup TmpDir=$(mktemp -d) rlRun "cd $TmpDir" 0 "进入临时测试目录" rlRun "touch testfile" 0 "创建测试文件" rlRun "mkdir testdir" 0 "创建测试目录" rlPhaseEnd rlPhaseStartTest "setfacl 基本功能" # 功能点 1: -m 设置 user ACL rlRun "setfacl -m u:root:rwx testfile" 0 "设置用户 ACL" rlRun "getfacl testfile 2>&1 | grep -q 'user:root:rwx'" 0 "确认 user ACL 已设置" # 功能点 2: -m 设置 group ACL rlRun "setfacl -m g:root:r-- testfile" 0 "设置用户组 ACL" # 功能点 3: -m 设置 mask rlRun "setfacl -m m::rwx testfile" 0 "设置 mask" rlRun "getfacl testfile 2>&1 | grep -q 'mask::rwx'" 0 "确认 mask 已设置" # 功能点 4: -x 删除指定 ACL rlRun "setfacl -x g:root testfile" 0 "删除组 ACL 条目" rlRun "! getfacl testfile 2>&1 | grep -q 'group:root:'" 0 "确认组 ACL 已删除" # 功能点 5: -b 删除所有扩展 ACL rlRun "setfacl -b testfile" 0 "删除所有扩展 ACL" rlRun "! getfacl testfile 2>&1 | grep -qE '(user:|group:)(?!:[:space:]|::)'" 0 \ "确认所有命名 ACL 已清除" rlPhaseEnd rlPhaseStartCleanup "清理测试环境" rlRun "cd /" 0 "离开测试目录" [ -n "$TmpDir" ] && [ -d "$TmpDir" ] && rlRun "rm -rf $TmpDir" 0 "清理临时测试目录" rlPhaseEnd rlJournalPrintText rlJournalEnd ``` #### 9.4.3 用例 3: 错误处理 ```bash mkdir -p tests/functional/pkgs/acl/test_acl_error_handling ``` **`test.sh`** — 使用 `rlRun` 的 `expect=1` 模式验证错误路径: ```bash #!/bin/bash # Functional test: acl - error handling . /usr/share/beakerlib/beakerlib.sh || exit 1 . "$(dirname "$0")/../lib.sh" rlJournalStart rlPhaseStartSetup "环境准备" aclSetup TmpDir=$(mktemp -d) rlRun "cd $TmpDir" 0 "进入临时测试目录" rlRun "touch testfile" 0 "创建测试文件" rlPhaseEnd rlPhaseStartTest "错误处理" # 功能点 1: 不存在的文件 — 期望失败 (exit 1) rlRun "getfacl /nonexistent 2>&1 | grep -qiE \"error|Error|No such|无法|not found\" || echo expected-error" 1 \ "查看不存在的文件" # 功能点 2: 不存在的用户 — 期望失败 rlRun "setfacl -m u:nobody_xxx:rwx testfile 2>&1 | grep -qiE \"error|Error|Invalid|无效\" || echo expected-error" 1 \ "设置不存在的用户" # 功能点 3: 无效权限 — 期望失败 rlRun "setfacl -m u:root:abc testfile 2>&1 | grep -qiE \"error|Error|Invalid|无效\" || echo expected-error" 1 \ "设置无效权限字符串" rlPhaseEnd rlPhaseStartCleanup "清理测试环境" rlRun "cd /" 0 "离开测试目录" [ -n "$TmpDir" ] && [ -d "$TmpDir" ] && rlRun "rm -rf $TmpDir" 0 "清理临时测试目录" rlPhaseEnd rlJournalPrintText rlJournalEnd ``` ### 9.5 第 4 步:本地验证 ```bash cd ~/openruyi-autotest # 1. 确认 tmt 能发现新测试 tmt test ls | grep acl # 预期输出: # /tests/functional/pkgs/acl/test_acl_getfacl_basic # /tests/functional/pkgs/acl/test_acl_setfacl_basic # /tests/functional/pkgs/acl/test_acl_error_handling # 2. 先手动执行验证语法 bash tests/functional/pkgs/acl/test_acl_getfacl_basic/test.sh # 3. 通过 tmt 执行 tmt run --all plan --name /plans/functional \ test --name /tests/functional/pkgs/acl \ provision --feeling-safe # 4. 查看汇总结果 tmt run --last report ``` ### 9.6 ACL 测试套完整清单 | # | 用例 | 功能点 | 覆盖命令 | |---|------|--------|----------| | 1 | `test_acl_acl_inheritance` | 默认 ACL 继承、目录下新建文件继承 | getfacl | | 2 | `test_acl_acl_permission_verify` | ACL 权限实际生效(读/写/执行) | setfacl, getfacl | | 3 | `test_acl_chacl_command` | chacl 命令基本操作 | chacl | | 4 | `test_acl_error_handling` | 不存在的文件/用户、无效权限 | getfacl, setfacl | | 5 | `test_acl_getfacl_basic` | 7 个 getfacl 参数 (-a/-d/-c/-n/-t) | getfacl | | 6 | `test_acl_setfacl_advanced` | default ACL、--set、-M 从文件读取 | setfacl | | 7 | `test_acl_setfacl_basic` | -m/-x/-b 增删改查 | setfacl | | 8 | `test_acl_setfacl_recursive` | -R 递归设置 | setfacl | | 9 | `test_acl_setfacl_remove` | -x 按条目删除、-X 文件批量删除 | setfacl | | 10 | `test_acl_setfacl_symlink` | -L/-P 符号链接处理 | setfacl | | 11 | `test_acl_special_cases` | 多用户多组、--test 试运行、备份恢复 | setfacl, getfacl | > **关键设计决策**:每个用例只测试一个明确的主题(如 "getfacl 基本功能"),避免一个巨长脚本涵盖所有功能。这样单独执行/调试更方便,tmt 报告也更精确。 ### 9.7 开发检查清单 对照第 7 节的验证清单,逐项确认: - [x] 每个 `main.fmf` 包含 summary, test, tag, duration, tier, path, require - [x] `tag` 包含 `functional` (Plan filter) 和 `acl` (软件包标签) - [x] `require` 包含 `acl` + `coreutils` + `beakerlib` - [x] 每个 `test.sh` 首行 `#!/bin/bash`,sources beakerlib.sh 和 ../lib.sh - [x] Setup: 调用 `aclSetup` → 创建 `$TmpDir` → `cd $TmpDir` - [x] Test: 每个功能点至少一个 `rlRun` 或 `rlAssertGrep` - [x] Cleanup: `cd /` → `rm -rf $TmpDir` → 共享库自动卸载 - [x] 错误路径用例使用 `rlRun "cmd" 1`(期望失败) - [x] 所有用例独立可运行(依赖共享库但不需要其他用例的输出)