Each feature added to clan should be tested extensively via automated tests.
This document covers different methods of automated testing, including creating, running and debugging such tests.
In order to test the behavior of clan, different testing frameworks are used depending on the concern:
The NixOS VM Testing Framework is used to create high level integration tests, by running one or more VMs generated from a specified config. Commands can be executed on the booted machine(s) to verify a deployment of a service works as expected. All machines within a test are connected by a virtual network. Internet access is not available.
NixOS VM Tests are slow and expensive. They should only be used for testing high level integration of components. VM tests should be avoided wherever it is possible to implement a cheaper unit test instead.
Existing nixos vm tests in clan-core can be found by using ripgrep:
rg self.clanLib.test.baseTest All nixos vm tests in clan are exported as individual flake outputs under checks.x86_64-linux.{test-attr-name}.
If a test fails in CI:
gitea:clan/clan-core#checks.x86_64-linux.borgbackup/1242checks.x86_64-linux.borgbackup is the attribute pathborgbackup/checks directory via ripgrepexample: locating the vm test named borgbackup:
$ rg "borgbackup =" ./checks
./checks/flake-module.nix
44- wayland-proxy-virtwl = self.clanLib.test.baseTest ./wayland-proxy-virtwl nixosTestArgs; -> the location of that test is /checks/flake-module.nix line 41.
Create a nixos test module under /checks/{name}/default.nix and import it in /checks/flake-module.nix.
nix build .#checks.x86_64-linux.{test-attr-name} (replace {test-attr-name} with the name of the test)
Services that define their own vars (using clan.core.vars.generators) require generating test vars before running the tests.
clan.directory settingThe clan.directory option is critical for vars generation and loading in tests. This setting determines:
update-vars, it creates vars/ and sops/ directories inside the path specified by clan.directoryclan.directoryFor services that define vars, you must first run:
nix run .#checks.x86_64-linux.{test-attr-name}.update-vars This generates the necessary var files in the directory specified by clan.directory. After running this command, you can run the test normally:
nix run .#checks.x86_64-linux.{test-attr-name} The service-dummy-test is a good example of a test that uses vars. To run it:
# First, generate the test vars
nix run .#checks.x86_64-linux.service-dummy-test.update-vars
# Then run the test
nix run .#checks.x86_64-linux.service-dummy-test If update-vars fails, you may need to ensure that:
clan.directory is set correctly: It should point to the directory where you want vars to be generated (typically clan.directory = ./.; in your test definition)clan.inventory.machines or through the inventory systemIf vars are not found during test execution:
clan.directory points to the same location where you ran update-varsvars/ and sops/ directories exist in that locationYou can reference /checks/service-dummy-test/ to see a complete working example of a test with vars, including the correct directory structure.
The following techniques can be used to debug a VM test:
Locate the definition (see above) and add print statements, like, for example print(client.succeed("systemctl --failed")), then re-run the test via nix build (see above)
nix run .#checks.x86_64-linux.{test-attr-name}.driver -- --interactive start_all()
machine1.succeed("echo hello")
To get an interactive shell at a specific line in the VM test script, add a breakpoint() call before the line to debug, then run the test outside of the sandbox via: nix run .#checks.x86_64-linux.{test-attr-name}.driver
Those are very similar to NixOS VM tests, as in they run virtualized nixos machines, but instead of using VMs, they use containers which are much cheaper to launch. As of now the container test driver is a downstream development in clan-core. Basically everything stated under the NixOS VM tests sections applies here, except some limitations.
Container tests are enabled by default for all tests using the clan testing framework. They offer significant performance advantages over VM tests:
To control whether a test uses containers or VMs, use the clan.test.useContainers option:
{
clan = {
directory = ./.;
test.useContainers = true; # Use containers (default)
# test.useContainers = false; # Use VMs instead
};
} When to use VM tests instead of container tests:
Container tests require the uid-range system feature** in the Nix sandbox.
This feature allows Nix to allocate a range of UIDs for containers to use, enabling systemd-nspawn containers to run properly inside the Nix build sandbox.
Configuration:
The uid-range feature requires the auto-allocate-uids setting to be enabled in your Nix configuration.
To verify or enable it, add to your /etc/nix/nix.conf or NixOS configuration:
settings.experimental-features = [
"auto-allocate-uids"
];
nix.settings.auto-allocate-uids = true;
nix.settings.system-features = [ "uid-range" ]; Technical details:
requiredSystemFeatures = [ "uid-range" ]; in their derivation (see lib/test/container-test-driver/driver-module.nix:98)Existing NixOS container tests in clan-core can be found by using ripgrep:
rg self.clanLib.test.containerTest Since the Clan CLI is written in python, the pytest framework is used to define unit tests and integration tests via python
Due to superior efficiency,
Existing python tests in clan-core can be found by using ripgrep:
rg "import pytest" If any python test fails in the CI pipeline, an error message like this can be found at the end of the log:
...
FAILED tests/test_machines_cli.py::test_machine_delete - clan_lib.errors.ClanError: Template 'new-machine' not in 'inputs.clan-core
... In this case the test is defined in the file /tests/test_machines_cli.py via the test function test_machine_delete.
If a specific python module is tested, the test should be located near the tested module in a subdirectory called ./tests If the test is not clearly related to a specific module, put it in the top-level ./tests directory of the tested python package. For clan-cli this would be /pkgs/clan-cli/clan_cli/tests.
All filenames must be prefixed with test_ and test functions prefixed with test_ for pytest to discover them.
To run all python tests which are executed in the CI pipeline locally, use this nix build command
nix build .#checks.x86_64-linux.clan-pytest-{with,without}-core To run a specific python test outside the nix sandbox
/pkgs/clan-cli)select-shell {package} in the top-level dev shell of clan-core, (eg. switch-shell clan-cli)pytest ./path/to/test_file.py:test_function_name -s -n0The flags -sn0 are useful to forwards all stdout/stderr output to the terminal and be able to debug interactively via breakpoint().
To debug a specific python test, find its definition (see above) and make sure to enter the correct dev environment for that python package.
Modify the test and add breakpoint() statements to it.
Execute the test using the flags -sn0 in order to get an interactive shell at the breakpoint:
pytest ./path/to/test_file.py:test_function_name -sn0
Nix eval tests are good for testing any nix logic, including
When not to use
Existing nix eval tests can be found via this ripgrep command:
rg "nix-unit --eval-store" Failing nix eval tests look like this:
> ✅ test_attrsOf_attrsOf_submodule
> ✅ test_attrsOf_submodule
> ❌ test_default
> /build/nix-8-2/expected.nix --- Nix
> 1 { foo = { bar = { __prio = 1500; }; } 1 { foo = { bar = { __prio = 1501; }; }
> . ; } . ; }
>
>
> ✅ test_no_default
> ✅ test_submodule
> ✅ test_submoduleWith
> ✅ test_submodule_with_merging
>
> 😢 6/7 successful
> error: Tests failed To locate the definition, find the flake attribute name of the failing test near the top of the CI Job page, like for example gitea:clan/clan-core#checks.x86_64-linux.eval-lib-values/1242.
In this case eval-lib-values is the attribute we are looking for.
Find the attribute via ripgrep:
$ rg "eval-lib-values ="
lib/values/flake-module.nix
21: eval-lib-values = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
grmpf@grmpf-nix ~/p/c/clan-core (test-docs)> In this case the test is defined in the file lib/values/flake-module.nix line 21
In clan core, the following pattern is usually followed:
test.nix fileflake-module.nixflake-module.nix is imported via the flake.nix at the root of the projectFor example, see /lib/values/{test.nix,flake-module.nix}.
Since all nix eval tests are exposed via the flake outputs, they can be ran via nix build:
nix build .#checks.x86_64-linux.{test-attr-name} For quicker iteration times, instead of nix build use the nix-unit command available in the dev environment.
Example:
nix-unit --flake .#legacyPackages.x86_64-linux.{test-attr-name} Follow the instructions above to find the definition of the test, then use one of the following techniques:
Add lib.trace or lib.traceVal statements in order to print some variables during evaluation
Use nix repl to evaluate and inspect the test.
Each test consists of an expr (expression) and an expected field. nix-unit simply checks if expr == expected and prints the diff if that's not the case.
nix repl can be used to inspect an expr manually, or any other variables that you choose to expose.
Example:
$ nix repl
Nix 2.25.5
Type :? for help.
nix-repl> tests = import ./lib/values/test.nix {}
nix-repl> tests
{
test_attrsOf_attrsOf_submodule = { ... };
test_attrsOf_submodule = { ... };
test_default = { ... };
test_no_default = { ... };
test_submodule = { ... };
test_submoduleWith = { ... };
test_submodule_with_merging = { ... };
}
nix-repl> tests.test_default.expr
{
foo = { ... };
}