Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/vm-test-typecheck.nix
atlas 32f5b74ba4 ci: type-check vm test scripts without building the vms
Adds checks.<system>.vm-test-typecheck-<name> for hive-squid, swarm-archive, agent-ephemeral-rootfs, full-setup and wg-hive: ty and ruff F over nixpkgs' test-script prelude, the machine hints from the node names, and the script text. Node configs are never evaluated. vm-test-typecheck-control passes only when ty rejects a known type error.
2026-11-13 09:47:36 +01:00

140 lines
4.7 KiB
Nix

# `vm-test-typecheck-<name>` checks: run the type check and lint that nixpkgs'
# test driver applies to a VM test script (`ty check`, `ruff check --select F`)
# without building the driver. The driver derivation references every node's
# `system.build.vm`, so building or even evaluating it costs a full NixOS
# evaluation per node; the check itself needs only the script text and the
# machine names, and `nodes` is a lazy attribute set, so reading its names
# forces no node configuration.
{ pkgs, vmTests }:
let
inherit (pkgs) lib;
# nixos/lib/testing/driver.nix:24-31 (the pinned nixpkgs).
pythonizeName =
name:
let
head = lib.substring 0 1 name;
tail = lib.substring 1 (-1) name;
in
(if builtins.match "[A-z_]" head == null then "_" else head)
+ lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail;
# The type hints of nixos/lib/testing/driver.nix:22-53: one stub machine per
# node, one per container, and `machine` when it is the only node and no
# machine already has that name. The `vlanN` hints are left out: they need
# every node's `virtualisation.vlans`, and no script here uses a `vlanN` name.
hintsFor =
test:
let
nodeNames = lib.attrNames test.config.nodes;
containerNames = lib.attrNames test.config.containers;
alias = lib.optional (
lib.length nodeNames == 1 && !lib.elem "machine" (nodeNames ++ containerNames)
) "machine";
in
lib.concatStringsSep "\n" (
map (n: "${pythonizeName n} = create_fake_qemu_machine()") (nodeNames ++ alias)
++ map (n: "${pythonizeName n} = create_fake_nspawn_machine()") containerNames
);
# `test_driver` reaches ty through `buildInputs`, as in driver.nix:66.
checkInputs = {
nativeBuildInputs = [
pkgs.ty
pkgs.ruff
];
buildInputs = [ pkgs.nixos-test-driver ];
};
# The text driver.nix:77-82 assembles: the prelude from the pinned nixpkgs,
# the hints, then the script.
assemble = ''
cat ${pkgs.path}/nixos/lib/test-script-prepend.py > testScriptWithTypes
echo "$hints" >> testScriptWithTypes
echo -n "$testScript" >> testScriptWithTypes
'';
typecheck =
name: test:
pkgs.runCommand "vm-test-typecheck-${name}"
(
checkInputs
// {
testScript = test.config.testScriptString;
hints = hintsFor test;
}
)
''
${assemble}
ty check testScriptWithTypes
# driver.nix:91-99
ruff check --select F testScriptWithTypes
touch $out
'';
# Succeeds only when `ty` rejects an `Optional` dereference and three calls on
# the stub machines of `hintsFor`. If `test_driver` stops resolving, every
# name is `Unknown`, the real checks pass on anything, and this one fails.
control =
pkgs.runCommand "vm-test-typecheck-control"
(
checkInputs
// {
testScript = ''
import re
def first_group(s: str) -> str:
return re.search(r"x", s).group(1)
my_node.no_such_node()
machine.no_such_alias()
c_1.no_such_container()
'';
hints = hintsFor {
config = {
nodes."my-node" = { };
containers."c-1" = { };
};
};
}
)
''
${assemble}
if ty check testScriptWithTypes > ty.out 2>&1; then
cat ty.out
echo "ty accepted re.search(...).group(1); the type check is not effective" >&2
exit 1
fi
cat ty.out
# `group` comes from the stdlib stubs; the other three are attributes of
# the stub machines `hintsFor` declares, so they show the node, `machine`
# alias and container hints each resolve to a typed machine.
grep -q "Attribute .group. is not defined on .None." ty.out
grep -q "has no attribute .no_such_node." ty.out
grep -q "has no attribute .no_such_alias." ty.out
grep -q "has no attribute .no_such_container." ty.out
if grep -q unresolved-import ty.out; then
echo "test_driver does not resolve" >&2
exit 1
fi
touch $out
'';
# semi-spread and swarm-builder-offline are not listed: they interpolate node
# config or an agent toplevel into their script, so reading the script text
# evaluates a NixOS system. They need their script to be a function of those
# values first.
tests = {
inherit (vmTests)
hive-squid
swarm-archive
agent-ephemeral-rootfs
full-setup
wg-hive
;
};
in
(lib.mapAttrs' (n: t: lib.nameValuePair "vm-test-typecheck-${n}" (typecheck n t)) tests)
// {
vm-test-typecheck-control = control;
}