
NixOS 测试编写完全指南从 test module 到 testScript 的实战与源码解析【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgsNixOS 集成测试框架允许你用声明式的 Nix 模块定义一台或多台虚拟测试机器再用一段 Python 脚本驱动它们完成启动、断言与交互最终得到可重复构建的集成测试产物。本文以 Nixpkgs 仓库中 Writing Tests 官方文档为核心骨架结合仓库内测试驱动test-driver、测试模块系统nixos/lib/testing与真实测试用例如 login.nix、nat.nix的源码实现帮助你从零编写、调用、调试 NixOS 测试并理解底层工作原理。什么是 NixOS 测试模块一个 NixOS 测试本质上是一个测试模块test module其标准结构如下{ # QEMU virtual machines: nodes { vm1 { config, pkgs, ... }: { # ... }; vm2 { config, pkgs, ... }: { # ... }; # … }; # systemd-nspawn containers: containers { container1 { config, pkgs, ... }: { # ... }; container2 { config, pkgs, ... }: { # ... }; }; testScript Python code… ; }nodes.name与containers.name的值本身是标准的 NixOS 模块通过virtualisation.vlans等选项接入测试网络testScript是一段 Python 代码负责真正执行测试它会启动一台或多台虚拟机/systemd-nspawn容器并在其中运行命令、做断言。在仓库源码中这套结构被实现为class nixosTest的模块系统求值入口位于 nixos/lib/testing/default.nixtestModules依次加载call-test.nix、driver.nix、nodes.nix、testScript.nix等模块而 nixos/lib/testing-python.nix 提供了向后兼容的makeTest/runTest包装。单机测试实例nixos/tests/login.nix 只用一个nodes.machine节点验证用户能否在虚拟控制台登录、切换控制台时设备属主是否正确维护如getfacl /dev/snd/timer在 VT 切换前后的变化。多机测试实例nixos/tests/nfs/simple.nix 用两个客户端节点验证服务器崩溃场景下的文件锁正确性。测试可以同时包含虚拟机和容器只要它们被配置到同一个 VLAN就能通过网络互相访问仓库中的相关示例见 nixos/tests/containers.nix原文档指向的历史链接在当前仓库中对应此文件。调用一个测试测试的调用方式取决于它是 NixOS 仓库内的测试还是仓库之外其他项目中的测试。在 NixOS 仓库内测试NixOS 内置测试统一注册在 nixos/tests/all-tests.nix 中{ hostname runTest ./hostname.nix; }可以用匿名模块在all-tests.nix里附加覆盖override{ hostname runTest { imports [ ./hostname.nix ]; defaults.networking.firewall.enable false; }; }然后以属性名hostname运行cd /my/git/clone/of/nixpkgs nix-build -A nixosTests.hostname仓库内测试与仓库外测试在默认行为上有几处关键差异仓库内测试中pkgs.*默认只读可在测试层级用node.pkgsReadOnly false;解除对应 nixos/lib/testing/nodes.nix 中node.pkgsReadOnly选项的实现见 L265 附近nix.enable默认设为false用于缩小构建闭包尤其避免nix及其依赖的巨大反向闭包。在 NixOS 项目之外测试nixpkgs仓库之外的项目使用pkgs.testers中的runNixOSTest函数let pkgs import nixpkgs { }; in pkgs.testers.runNixOSTest { imports [ ./test.nix ]; defaults.services.foo.package mypkg; }runNixOSTest返回一个运行测试的 derivation。其求值同样经由 nixos/lib/testing/default.nix 的runTest模块系统求值后取出result config.test作为最终 derivation。仓库外测试使用一套最小惊讶原则的默认值具体差异仍以上述仓库内差异pkgs 只读、nix.enable默认关闭为准。测试机器虚拟机与容器一个 NixOS 测试通常由一台或多台测试机器组成每台机器要么是QEMU 虚拟机定义在nodes要么是systemd-nspawn 容器定义在containers。对所有机器统一生效的选项放在defaults中若想分别对虚拟机和容器设置不同的默认值使用nodeDefaults与containerDefaults。虚拟机 vs 容器的取舍容器的优势来自原文档与宿主机共享内核启动速度显著快于虚拟机资源占用更轻单台宿主机可并行运行更多实例易于在虚拟化环境中运行例如 CI 系统允许直接 bind-mount 宿主设备节点从而可以测试 GPU如 CUDA相关代码。虚拟机的优势运行独立内核可以测试内核特性内核模块等支持在 X11 上测试图形应用允许测试使用 systemd namespacing 选项如ProtectSystem、MountAPIVFS的 NixOS 模块允许测试specialisation切换 specialisation 需要创建 SUID/SGID wrapper这在 Nix 沙箱内的systemd-nspawn中是被禁止的允许执行setuid二进制。从驱动源码看两类机器在 test-driver 中分别实现为QemuMachine与NspawnMachine见BaseMachine抽象基类及 driver.py 中的machines_qemu/machines_nspawn列表两类机器共享同一套start/execute/wait_for_unit等接口。配置测试机器的网络与关键选项所有测试机器无论虚拟机还是容器都可以使用特殊选项virtualisation.vlans指定虚拟网络nat.nixnixos/tests/nat.nix是典型范例它用 VLAN 1 模拟内网、VLAN 2 模拟外网将client与router节点接入对应网络。其底层选项定义位于 nixos/modules/virtualisation/guest-networking-options.nix。systemd-nspawn 容器专属选项virtualisation.systemd-nspawn.options启动容器时传递给systemd-nspawn的额外命令行选项列表。例如把宿主机目录 bind-mount 进容器virtualisation.systemd-nspawn.options [ --bind/host/dir:/container/dir ];该选项在 nixos/modules/virtualisation/nspawn-container/default.nix 中定义并经由run-nspawn与lib.escapeShellArgs config.virtualisation.systemd-nspawn.options拼入实际启动命令L125 附近。⚠️沙箱注意事项--bind/--bind-ro中引用的路径必须能在 Nix 沙箱内访问。需要在宿主机侧通过 Nix 选项sandbox-paths和/或programs.nix-required-mounts模块把额外路径加入沙箱。仓库还专门为容器场景提供 nixos/tests/nix-required-mounts 测试验证这一机制。QEMU 虚拟机专属选项virtualisation.memorySizeVM 内存大小单位 MiB1024×1024 字节。实现见 nixos/modules/virtualisation/qemu-vm.nix在 L357 处以-m ${toString config.virtualisation.memorySize}传入 QEMU并在 L1146 附近断言 32 位系统下最大 2047 MiB 的限制。virtualisation.writableStore默认 VM 内 Nix store 不可写启用后会在 Nix store 之上挂载可写的 union 文件系统使其看似可写。这对于运行会修改 store 的 Nix 操作如nix-env -i的测试是必需的。其实现同样位于 qemu-vm.nixL718 附近相关衍生选项还有virtualisation.writableStoreUseTmpfs。其余 QEMU 相关选项可查阅 nixos/modules/virtualisation/qemu-vm.nix 模块。编写 testScriptPython 驱动的测试逻辑testScript是一系列 Python 语句执行各种动作启动机器、在机器内执行命令、断言输出等。例如你在nodes.machine定义了一台虚拟机testScript 中就会有一个同名的 Python 变量machinemachine.start() machine.wait_for_unit(default.target) t.assertIn(Linux, machine.succeed(uname), Wrong OS)关键点第一行machine.start()技术上可以省略——机器会在第一次执行动作如wait_for_unit、succeed时被隐式启动多机场景可用start_all()并行启动所有机器以加快测试变量t提供了unittest.TestCase的全部断言方法源码见 driver.py 中的AssertionTester其failureException被定制为RequestedAssertionFailed便于日志特殊处理若机器主机名含不能作为 Python 变量名的字符会被替换为下划线例如nodes.machine-a在 Python 中暴露为machine_a。这一替换逻辑在 driver.py 的pythonize_nameL87-88以及 driver.nix 的类型提示生成L27-35中都有体现。机器对象上可用的方法机器对象如machine支持的方法包括原文档通过PYTHON_MACHINE_METHODS注入此处结合 test-driver 源码 归纳核心几类命令执行succeed(...)执行命令并要求退出码为 0返回 stdoutfail(...)恰好相反execute(cmd)返回(status, stdout)元组。注意所有命令都在set -euo pipefail语义下运行见execute文档字符串L685-718分离daemonize的命令必须关闭 stdout重定向到2、/dev/console、/dev/null或文件否则execute会一直等待其 stdout 关闭。等待类wait_for_unit(unit)等待 systemd 单元进入active状态单元进入failed/inactive会抛异常wait_until_succeeds(cmd)/wait_until_fails(cmd)以 1 秒间隔重试直到成功/失败wait_for_file(path)等待文件出现wait_for_open_port(port, addr)/wait_for_closed_port(...)用nc -z探测 TCP 端口wait_for_open_unix_socket(addr)支持 UNIX domain socket。systemd 交互systemctl(q, userNone)执行systemctl带user时走systemctl --userstart_job/stop_job启停服务get_unit_info/get_unit_property通过systemctl show读取单元属性。文件传递copy_from_machine(source, target_dir)从机器拷贝文件到$out通过各机器共享的shared_dir中转copy_from_host(source, target)/copy_from_host_via_shell(...)把宿主文件写入机器。其它sleep在客户机时间内休眠shutdown()优雅关机、wait_for_shutdown()等待关机screenshot(...)截图配合 OCR 使用。所有轮询/等待类方法都有默认 15 分钟超时见 machine/init.py 中retry的默认dt.timedelta(minutes15)可用timeoutdt.timedelta(...)参数覆盖。测试用户级user单元要测试systemd.user.services声明的用户单元可以使用可选参数usermachine.start() machine.wait_for_x() machine.wait_for_unit(xautolock.service, x-session-user)该参数适用于systemctl、get_unit_info、wait_for_unit、start_job和stop_job。底层实现中带user的调用会以su -l user --shell /bin/sh -c ... systemctl --user ...的方式执行见 machine/init.py 的systemctl方法L349-370。用 polling_condition 提前失败当某些不变量不再满足时可以用装饰器polling_condition让测试提前失败而不是等构建超时。例如测试程序foo启动后不应退出polling_condition def foo_running(): machine.succeed(pgrep -x foo) machine.succeed(foo --start) machine.wait_until_succeeds(pgrep -x foo) with foo_running: ... # Put foo through its pacespolling_condition的可选参数interval轮询间隔类型为datetime.timedeltaimport datetime as dt polling_condition(intervaldt.timedelta(seconds10)) def foo_running(): machine.succeed(pgrep -x foo)description日志中显示的条件描述未提供时取自函数 docstring。以下两种写法等价polling_condition def foo_running(): check that foo is running machine.succeed(pgrep -x foo)polling_condition(descriptioncheck that foo is running) def foo_running(): machine.succeed(pgrep -x foo)实现层面polling_condition由 nixos/lib/test-driver/src/test_driver/polling_condition.py 中的PollingCondition类支撑默认间隔dt.timedelta(seconds2)description依次回退到函数 docstring 或函数名L44-53进入with块后条件不满足即抛出PollingConditionErrorL79-81。驱动通过callbacks[self.check_polling_conditions]把条件检查挂到每次机器动作上见 driver.py L187-200。向 testScript 添加 Python 包当 testScript 需要额外 Python 库时使用extraPythonPackages参数。例如引入numpy{ extraPythonPackages p: [ p.numpy ]; nodes { }; # Type checking on extra packages doesnt work yet skipTypeCheck true; testScript import numpy as np assert str(np.zeros(4)) [0. 0. 0. 0.] ; }此时numpy取自通用的python3Packages。该选项的类型定义在 nixos/lib/testing/driver.nixextraPythonPackages类型为functionTo (listOf package)并会被传入pythonTestDriverPackage.override参与驱动打包L21-24。testScript 的 lint 与类型检查testScript 会自动执行 Pyflakes 风格 lint 和 Mypy 风格类型检查任何 lint/类型错误都会导致测试求值失败。跳过 lint仅用于快速迭代不应提交{ skipLint true; nodes.machine { config, pkgs, ... }: { # configuration… }; testScript Python code… ; }这会触发求值期 Nix 警告。要完全禁用格式化可在 testScript 中用注释指令包住 Black 格式化器同样不要提交到 Nixpkgs 仓库{ testScript # fmt: off Python code… # fmt: on ; }跳过类型检查{ skipTypeCheck true; nodes.machine { config, pkgs, ... }: { # configuration… }; }源码实现见 nixos/lib/testing/driver.nix构建驱动时通过cat ../test-script-prepend.py testScriptWithTypes预置类型提示再分别执行ty check类型检查L90-95与ruff check --select F testScriptWithTypesF 系列即 pyflakes 检查L97-105。类型提示本身由 driver.nix 根据driverConfiguration的 vms/containers/vlans 名自动生成pythonizeNamecreate_fake_qemu_machine()等桩声明。覆盖Override一个测试NixOS 测试框架为测试返回带多种覆盖方法的结果。overrideTestDerivationfunction: 相当于对测试 derivation 应用overrideAttrs。这是extend配合对rawTestDerivationArg选项做 override 的便捷封装。*function*一个扩展函数形如 finalAttrs: prevAttrs: { /* … */ }其结果传给 mkDerivation与 overrideAttrs 一样也支持缩写形式如 prevAttrs: { /* … */ } 甚至 { /* … */ }。参见 lib.extends。extendNixOS { module *module*; specialArgs *specialArgs*; }: 以额外的 NixOS 模块和/或参数重新求值测试。- module要加入所有测试机器的 NixOS 模块设置测试选项 extraBaseModules - specialArgs传给所有 NixOS 模块的参数属性集会覆盖既有参数以及模块可能定义的任何 _module.args.name设置测试选项 node.specialArgs。 这是 extend 覆盖上述测试选项的便捷函数。官方示例在 passthru.tests 中使用 extendNixOS使 (openssh.tests.overrideAttrs f).tests.nixos 保持一致 nix mkDerivation (finalAttrs: { # … passthru { tests { nixos nixosTests.openssh.extendNixOS { module { services.openssh.package finalAttrs.finalPackage; }; }; }; }; }) extend { modules *modules*; specialArgs *specialArgs*; }: 为测试添加新的nixosTest模块和/或模块参数与既有模块及内置选项见下节一起求值。如果只是想扩展测试的 *NixOS 配置* 而非测试本身的其它部分应优先使用 extendNixOS 便捷函数。 - modules要加入测试的模块列表会与既有模块一起被 evalModules 求值 - specialArgs传给测试的参数属性集会覆盖既有参数及模块定义的 _module.args.name。这些扩展机制在源码中的落脚点包括 nixos/lib/testing/nodes.nixextraBaseModules、node.specialArgs、node.pkgsReadOnly选项见 L236-346以及测试驱动模块的extendModules机制。调试测试机器断点与 SSH 后门设置enableDebugHook选项可以让测试在第一次失败时暂停并打印如何进入测试沙箱 shell 的说明。例如{ name foo; nodes.machine { }; enableDebugHook true; sshBackdoor.enable true; testScript start_all() machine.succeed(false) # this will fail ; }测试失败时输出类似vm-test-run-foo !!! Breakpoint reached, run sudo /nix/store/eeeee-attach/bin/attach PID然后进入沙箱 shell$ sudo /nix/store/eeeee-attach/bin/attach PID bash#在沙箱 shell 里可以连接pdb会话逐步调试 Python 测试脚本bash# telnet 127.0.0.1 4444 pdb$注意也可以在 testScript 中用debug.breakpoint()直接设置断点。调试相关的 Python 实现见 nixos/lib/test-driver/src/test_driver/debug.py 与 driver.py--debug-hook-attach参数L116-117、L167-169。通过 SSH 访问测试虚拟机:::note 要 SSH 进测试机器调试建议优先使用[交互式驱动](#交互式运行与 ssh 后门)及其 SSH 后门。本功能主要面向难以在别处复现的 flaky间歇性失败测试的调试。 :::设置sshBackdoor.enable后QEMU 虚拟机将打开基于 AF_VSOCK 的 SSH 后门。进入沙箱 shell 后可以通过 vsock 访问虚拟机例如machinebash# ssh -F ./ssh_config -o Userroot vsock-mux//tmp/.../machine_host.socketsocket 路径会在测试开始时打印。VSOCK 基础设施由 driver.py 的VHostDeviceVsock/VsockPair实现L99-135每个 VM 按enumerate(machines, start3)分配 guest CID并通过vhost-device-vsock建立 host/guest socket 对。通过 SSH 访问测试容器同样设置sshBackdoor.enable后每个systemd-nspawn容器也会打开 SSH 后门。容器启动时会打印通过 SSH 登录容器的指令若测试失败按上述方法附加到沙箱然后用提供的 SSH 命令登录容器。例如$ sudo /nix/store/eeeee-attach PID bash# ssh -o Userroot -o ProxyCommandsocat - UNIX-CLIENT:/run/systemd/nspawn/unix-export/machine/ssh bash [rootmachine:~]# hostname machine交互式运行与 SSH 后门除沙箱调试外测试驱动还支持交互式运行nixos-test-driver提供了--interactive参数可进入 Python REPL 逐条执行测试语句见 test-driver__init__.pyL111-112 的参数定义与 L184 的交互入口。交互式模式配合sshBackdoor是日常调试测试脚本最顺手的途径你可以手动machine.start()、machine.succeed(...)实时观察机器状态而无需跑完整构建。测试选项参考与底层求值链框架为测试编写提供了完整的选项集原文档以id-prefix: test-opt-的选项列表形式给出构建期由NIXOS_TEST_OPTIONS_JSON注入。这些选项主要来自 nixos/lib/testing 目录下的模块nodes.nix节点/容器定义、extraBaseModules、node.specialArgs、node.pkgsReadOnly、driver.nixglobalTimeout、enableOCR、extraPythonPackages、skipLint、skipTypeCheck、logLevel等、run.nixrequiredFeatures包括devnet/uid-range/kvm等特性自动推断、testScript.nixtestScript及其字符串化与 store 引用收集。从整体链路看一次测试运行大致经历模块求值runTest/evalTest以class nixosTest求值测试模块testing/default.nix驱动打包按节点配置生成driverConfiguration.json把 testScript 与类型提示合并执行ty类型检查与rufflint 后用makeWrapper包装出nixos-test-driver可执行程序driver.nix驱动运行Python 驱动读取配置启动 VLAN、QEMU 虚拟机与 nspawn 容器按 testScript 顺序执行动作与断言driver.py 与 machine/init.py产物输出测试通过后$out目录保留截图、copy_from_machine拷贝出的文件等测试产物。掌握了 test module 的结构、机器配置选项、testScript 的完整方法集以及调试手段你就可以在 Nixpkgs 仓库内或自己的项目中写出可靠、可复现的 NixOS 集成测试并借助extendNixOS/overrideTestDerivation在包层面复用它们。【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考