1{ system
2, pkgs ? import ../.. { inherit system config; }
3 # Use a minimal kernel?
4, minimal ? false
5 # Ignored
6, config ? { }
7 # !!! See comment about args in lib/modules.nix
8, specialArgs ? { }
9 # Modules to add to each VM
10, extraConfigurations ? [ ]
11}:
12
13with pkgs;
14
15rec {
16
17 inherit pkgs;
18
19 # Reifies and correctly wraps the python test driver for
20 # the respective qemu version and with or without ocr support
21 pythonTestDriver = {
22 qemu_pkg ? pkgs.qemu_test
23 , enableOCR ? false
24 }:
25 let
26 name = "nixos-test-driver";
27 testDriverScript = ./test-driver/test-driver.py;
28 ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; };
29 imagemagick_tiff = imagemagick_light.override { inherit libtiff; };
30 in stdenv.mkDerivation {
31 inherit name;
32
33 nativeBuildInputs = [ makeWrapper ];
34 buildInputs = [ (python3.withPackages (p: [ p.ptpython p.colorama ])) ];
35 checkInputs = with python3Packages; [ pylint black mypy ];
36
37 dontUnpack = true;
38
39 preferLocalBuild = true;
40
41 buildPhase = ''
42 python <<EOF
43 from pydoc import importfile
44 with open('driver-symbols', 'w') as fp:
45 t = importfile('${testDriverScript}')
46 d = t.Driver([],[],"")
47 test_symbols = d.test_symbols()
48 fp.write(','.join(test_symbols.keys()))
49 EOF
50 '';
51
52 doCheck = true;
53 checkPhase = ''
54 mypy --disallow-untyped-defs \
55 --no-implicit-optional \
56 --ignore-missing-imports ${testDriverScript}
57 pylint --errors-only ${testDriverScript}
58 black --check --diff ${testDriverScript}
59 '';
60
61 installPhase =
62 ''
63 mkdir -p $out/bin
64 cp ${testDriverScript} $out/bin/nixos-test-driver
65 chmod u+x $out/bin/nixos-test-driver
66 # TODO: copy user script part into this file (append)
67
68 wrapProgram $out/bin/nixos-test-driver \
69 --argv0 ${name} \
70 --prefix PATH : "${lib.makeBinPath [ qemu_pkg vde2 netpbm coreutils socat ]}" \
71 ${lib.optionalString enableOCR
72 "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
73
74 install -m 0644 -vD driver-symbols $out/nix-support/driver-symbols
75 '';
76 };
77
78 # Run an automated test suite in the given virtual network.
79 runTests = { driver, pos }:
80 stdenv.mkDerivation {
81 name = "vm-test-run-${driver.testName}";
82
83 requiredSystemFeatures = [ "kvm" "nixos-test" ];
84
85 buildCommand =
86 ''
87 mkdir -p $out
88
89 # effectively mute the XMLLogger
90 export LOGFILE=/dev/null
91
92 ${driver}/bin/nixos-test-driver
93 '';
94
95 passthru = driver.passthru // {
96 inherit driver;
97 };
98
99 inherit pos; # for better debugging
100 };
101
102 # Generate convenience wrappers for running the test driver
103 # has vlans, vms and test script defaulted through env variables
104 # also instantiates test script with nodes, if it's a function (contract)
105 setupDriverForTest = {
106 testScript
107 , testName
108 , nodes
109 , qemu_pkg ? pkgs.qemu_test
110 , enableOCR ? false
111 , skipLint ? false
112 , passthru ? {}
113 }:
114 let
115 # FIXME: get this pkg from the module system
116 testDriver = pythonTestDriver { inherit qemu_pkg enableOCR;};
117
118 testDriverName =
119 let
120 # A standard store path to the vm monitor is built like this:
121 # /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor
122 # The max filename length of a unix domain socket is 108 bytes.
123 # This means $name can at most be 50 bytes long.
124 maxTestNameLen = 50;
125 testNameLen = builtins.stringLength testName;
126 in with builtins;
127 if testNameLen > maxTestNameLen then
128 abort
129 ("The name of the test '${testName}' must not be longer than ${toString maxTestNameLen} " +
130 "it's currently ${toString testNameLen} characters long.")
131 else
132 "nixos-test-driver-${testName}";
133
134 vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
135 vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
136
137 nodeHostNames = let
138 nodesList = map (c: c.config.system.name) (lib.attrValues nodes);
139 in nodesList ++ lib.optional (lib.length nodesList == 1) "machine";
140
141 # TODO: This is an implementation error and needs fixing
142 # the testing famework cannot legitimately restrict hostnames further
143 # beyond RFC1035
144 invalidNodeNames = lib.filter
145 (node: builtins.match "^[A-z_]([A-z0-9_]+)?$" node == null)
146 nodeHostNames;
147
148 testScript' =
149 # Call the test script with the computed nodes.
150 if lib.isFunction testScript
151 then testScript { inherit nodes; }
152 else testScript;
153
154 in
155 if lib.length invalidNodeNames > 0 then
156 throw ''
157 Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
158 All machines are referenced as python variables in the testing framework which will break the
159 script when special characters are used.
160
161 This is an IMPLEMENTATION ERROR and needs to be fixed. Meanwhile,
162 please stick to alphanumeric chars and underscores as separation.
163 ''
164 else lib.warnIf skipLint "Linting is disabled" (runCommand testDriverName
165 {
166 inherit testName;
167 nativeBuildInputs = [ makeWrapper ];
168 testScript = testScript';
169 preferLocalBuild = true;
170 passthru = passthru // {
171 inherit nodes;
172 };
173 }
174 ''
175 mkdir -p $out/bin
176
177 vmStartScripts=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
178 echo -n "$testScript" > $out/test-script
179 ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
180
181 ${lib.optionalString (!skipLint) ''
182 PYFLAKES_BUILTINS="$(
183 echo -n ${lib.escapeShellArg (lib.concatStringsSep "," nodeHostNames)},
184 < ${lib.escapeShellArg "${testDriver}/nix-support/driver-symbols"}
185 )" ${python3Packages.pyflakes}/bin/pyflakes $out/test-script
186 ''}
187
188 # set defaults through environment
189 # see: ./test-driver/test-driver.py argparse implementation
190 wrapProgram $out/bin/nixos-test-driver \
191 --set startScripts "''${vmStartScripts[*]}" \
192 --set testScript "$out/test-script" \
193 --set vlans '${toString vlans}'
194 '');
195
196 # Make a full-blown test
197 makeTest =
198 { testScript
199 , enableOCR ? false
200 , name ? "unnamed"
201 # Skip linting (mainly intended for faster dev cycles)
202 , skipLint ? false
203 , passthru ? {}
204 , # For meta.position
205 pos ? # position used in error messages and for meta.position
206 (if t.meta.description or null != null
207 then builtins.unsafeGetAttrPos "description" t.meta
208 else builtins.unsafeGetAttrPos "testScript" t)
209 , ...
210 } @ t:
211 let
212 nodes = qemu_pkg:
213 let
214 testScript' =
215 # Call the test script with the computed nodes.
216 if lib.isFunction testScript
217 then testScript { nodes = nodes qemu_pkg; }
218 else testScript;
219
220 build-vms = import ./build-vms.nix {
221 inherit system lib pkgs minimal specialArgs;
222 extraConfigurations = extraConfigurations ++ [(
223 { config, ... }:
224 {
225 virtualisation.qemu.package = qemu_pkg;
226
227 # Make sure all derivations referenced by the test
228 # script are available on the nodes. When the store is
229 # accessed through 9p, this isn't important, since
230 # everything in the store is available to the guest,
231 # but when building a root image it is, as all paths
232 # that should be available to the guest has to be
233 # copied to the image.
234 virtualisation.additionalPaths =
235 lib.optional
236 # A testScript may evaluate nodes, which has caused
237 # infinite recursions. The demand cycle involves:
238 # testScript -->
239 # nodes -->
240 # toplevel -->
241 # additionalPaths -->
242 # hasContext testScript' -->
243 # testScript (ad infinitum)
244 # If we don't need to build an image, we can break this
245 # cycle by short-circuiting when useNixStoreImage is false.
246 (config.virtualisation.useNixStoreImage && builtins.hasContext testScript')
247 (pkgs.writeStringReferencesToFile testScript');
248
249 # Ensure we do not use aliases. Ideally this is only set
250 # when the test framework is used by Nixpkgs NixOS tests.
251 nixpkgs.config.allowAliases = false;
252 }
253 )];
254 };
255 in
256 build-vms.buildVirtualNetwork (
257 t.nodes or (if t ? machine then { machine = t.machine; } else { })
258 );
259
260 driver = setupDriverForTest {
261 inherit testScript enableOCR skipLint passthru;
262 testName = name;
263 qemu_pkg = pkgs.qemu_test;
264 nodes = nodes pkgs.qemu_test;
265 };
266 driverInteractive = setupDriverForTest {
267 inherit testScript enableOCR skipLint passthru;
268 testName = name;
269 qemu_pkg = pkgs.qemu;
270 nodes = nodes pkgs.qemu;
271 };
272
273 test =
274 let
275 passMeta = drv: drv // lib.optionalAttrs (t ? meta) {
276 meta = (drv.meta or { }) // t.meta;
277 };
278 in passMeta (runTests { inherit driver pos; });
279
280 in
281 test // {
282 inherit test driver driverInteractive nodes;
283 };
284
285 abortForFunction = functionName: abort ''The ${functionName} function was
286 removed because it is not an essential part of the NixOS testing
287 infrastructure. It had no usage in NixOS or Nixpkgs and it had no designated
288 maintainer. You are free to reintroduce it by documenting it in the manual
289 and adding yourself as maintainer. It was removed in
290 https://github.com/NixOS/nixpkgs/pull/137013
291 '';
292
293 runInMachine = abortForFunction "runInMachine";
294
295 runInMachineWithX = abortForFunction "runInMachineWithX";
296
297 simpleTest = as: (makeTest as).test;
298
299}