1# Writing Tests {#sec-writing-nixos-tests} 2 3A NixOS test is a module that has the following structure: 4 5```nix 6{ 7 8 # One or more machines: 9 nodes = 10 { machine = 11 { config, pkgs, ... }: { /* ... */ }; 12 machine2 = 13 { config, pkgs, ... }: { /* ... */ }; 14 # … 15 }; 16 17 testScript = 18 '' 19 Python code… 20 ''; 21} 22``` 23 24We refer to the whole test above as a test module, whereas the values 25in [`nodes.<name>`](#test-opt-nodes) are NixOS modules themselves. 26 27The option [`testScript`](#test-opt-testScript) is a piece of Python code that executes the 28test (described below). During the test, it will start one or more 29virtual machines, the configuration of which is described by 30the option [`nodes`](#test-opt-nodes). 31 32An example of a single-node test is 33[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix). 34It only needs a single machine to test whether users can log in 35on the virtual console, whether device ownership is correctly maintained 36when switching between consoles, and so on. An interesting multi-node test is 37[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix). 38It uses two client nodes to test correct locking across server crashes. 39 40## Calling a test {#sec-calling-nixos-tests} 41 42Tests are invoked differently depending on whether the test is part of NixOS or lives in a different project. 43 44### Testing within NixOS {#sec-call-nixos-test-in-nixos} 45 46Tests that are part of NixOS are added to [`nixos/tests/all-tests.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/all-tests.nix). 47 48```nix 49{ 50 hostname = runTest ./hostname.nix; 51} 52``` 53 54Overrides can be added by defining an anonymous module in `all-tests.nix`. 55 56```nix 57{ 58 hostname = runTest { 59 imports = [ ./hostname.nix ]; 60 defaults.networking.firewall.enable = false; 61 }; 62} 63``` 64 65You can run a test with attribute name `hostname` in `nixos/tests/all-tests.nix` by invoking: 66 67```shell 68cd /my/git/clone/of/nixpkgs 69nix-build -A nixosTests.hostname 70``` 71 72### Testing outside the NixOS project {#sec-call-nixos-test-outside-nixos} 73 74Outside the `nixpkgs` repository, you can instantiate the test by first importing the NixOS library, 75 76```nix 77let nixos-lib = import (nixpkgs + "/nixos/lib") { }; 78in 79 80nixos-lib.runTest { 81 imports = [ ./test.nix ]; 82 hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs 83 defaults.services.foo.package = mypkg; 84} 85``` 86 87`runTest` returns a derivation that runs the test. 88 89## Configuring the nodes {#sec-nixos-test-nodes} 90 91There are a few special NixOS options for test VMs: 92 93`virtualisation.memorySize` 94 95: The memory of the VM in megabytes. 96 97`virtualisation.vlans` 98 99: The virtual networks to which the VM is connected. See 100 [`nat.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix) 101 for an example. 102 103`virtualisation.writableStore` 104 105: By default, the Nix store in the VM is not writable. If you enable 106 this option, a writable union file system is mounted on top of the 107 Nix store to make it appear writable. This is necessary for tests 108 that run Nix operations that modify the store. 109 110For more options, see the module 111[`qemu-vm.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix). 112 113The test script is a sequence of Python statements that perform various 114actions, such as starting VMs, executing commands in the VMs, and so on. 115Each virtual machine is represented as an object stored in the variable 116`name` if this is also the identifier of the machine in the declarative 117config. If you specified a node `nodes.machine`, the following example starts the 118machine, waits until it has finished booting, then executes a command 119and checks that the output is more-or-less correct: 120 121```py 122machine.start() 123machine.wait_for_unit("default.target") 124if not "Linux" in machine.succeed("uname"): 125 raise Exception("Wrong OS") 126``` 127 128The first line is technically unnecessary; machines are implicitly started 129when you first execute an action on them (such as `wait_for_unit` or 130`succeed`). If you have multiple machines, you can speed up the test by 131starting them in parallel: 132 133```py 134start_all() 135``` 136 137If the hostname of a node contains characters that can't be used in a 138Python variable name, those characters will be replaced with 139underscores in the variable name, so `nodes.machine-a` will be exposed 140to Python as `machine_a`. 141 142## Machine objects {#ssec-machine-objects} 143 144The following methods are available on machine objects: 145 146@PYTHON_MACHINE_METHODS@ 147 148To test user units declared by `systemd.user.services` the optional 149`user` argument can be used: 150 151```py 152machine.start() 153machine.wait_for_x() 154machine.wait_for_unit("xautolock.service", "x-session-user") 155``` 156 157This applies to `systemctl`, `get_unit_info`, `wait_for_unit`, 158`start_job` and `stop_job`. 159 160For faster dev cycles it's also possible to disable the code-linters 161(this shouldn't be committed though): 162 163```nix 164{ 165 skipLint = true; 166 nodes.machine = 167 { config, pkgs, ... }: 168 { # configuration… 169 }; 170 171 testScript = 172 '' 173 Python code… 174 ''; 175} 176``` 177 178This will produce a Nix warning at evaluation time. To fully disable the 179linter, wrap the test script in comment directives to disable the Black 180linter directly (again, don't commit this within the Nixpkgs 181repository): 182 183```nix 184{ 185 testScript = 186 '' 187 # fmt: off 188 Python code… 189 # fmt: on 190 ''; 191} 192``` 193 194Similarly, the type checking of test scripts can be disabled in the following 195way: 196 197```nix 198{ 199 skipTypeCheck = true; 200 nodes.machine = 201 { config, pkgs, ... }: 202 { # configuration… 203 }; 204} 205``` 206 207## Failing tests early {#ssec-failing-tests-early} 208 209To fail tests early when certain invariants are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following: 210 211```py 212@polling_condition 213def foo_running(): 214 machine.succeed("pgrep -x foo") 215 216 217machine.succeed("foo --start") 218machine.wait_until_succeeds("pgrep -x foo") 219 220with foo_running: 221 ... # Put `foo` through its paces 222``` 223 224`polling_condition` takes the following (optional) arguments: 225 226`seconds_interval` 227 228: specifies how often the condition should be polled: 229 230```py 231@polling_condition(seconds_interval=10) 232def foo_running(): 233 machine.succeed("pgrep -x foo") 234``` 235 236`description` 237 238: is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent: 239 240```py 241@polling_condition 242def foo_running(): 243 "check that foo is running" 244 machine.succeed("pgrep -x foo") 245``` 246 247```py 248@polling_condition(description="check that foo is running") 249def foo_running(): 250 machine.succeed("pgrep -x foo") 251``` 252 253## Adding Python packages to the test script {#ssec-python-packages-in-test-script} 254 255When additional Python libraries are required in the test script, they can be 256added using the parameter `extraPythonPackages`. For example, you could add 257`numpy` like this: 258 259```nix 260{ 261 extraPythonPackages = p: [ p.numpy ]; 262 263 nodes = { }; 264 265 # Type checking on extra packages doesn't work yet 266 skipTypeCheck = true; 267 268 testScript = '' 269 import numpy as np 270 assert str(np.zeros(4)) == "[0. 0. 0. 0.]" 271 ''; 272} 273``` 274 275In that case, `numpy` is chosen from the generic `python3Packages`. 276 277## Test Options Reference {#sec-test-options-reference} 278 279The following options can be used when writing tests. 280 281```{=include=} options 282id-prefix: test-opt- 283list-id: test-options-list 284source: @NIXOS_TEST_OPTIONS_JSON@ 285```