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
20 mkTestDriver =
21 let
22 testDriverScript = ./test-driver/test-driver.py;
23 in
24 qemu_pkg: stdenv.mkDerivation {
25 name = "nixos-test-driver";
26
27 nativeBuildInputs = [ makeWrapper ];
28 buildInputs = [ (python3.withPackages (p: [ p.ptpython p.colorama ])) ];
29 checkInputs = with python3Packages; [ pylint black mypy ];
30
31 dontUnpack = true;
32
33 preferLocalBuild = true;
34
35 buildPhase = ''
36 python <<EOF
37 from pydoc import importfile
38 with open('driver-exports', 'w') as fp:
39 fp.write(','.join(dir(importfile('${testDriverScript}'))))
40 EOF
41 '';
42
43 doCheck = true;
44 checkPhase = ''
45 mypy --disallow-untyped-defs \
46 --no-implicit-optional \
47 --ignore-missing-imports ${testDriverScript}
48 pylint --errors-only ${testDriverScript}
49 black --check --diff ${testDriverScript}
50 '';
51
52 installPhase =
53 ''
54 mkdir -p $out/bin
55 cp ${testDriverScript} $out/bin/nixos-test-driver
56 chmod u+x $out/bin/nixos-test-driver
57 # TODO: copy user script part into this file (append)
58
59 wrapProgram $out/bin/nixos-test-driver \
60 --prefix PATH : "${lib.makeBinPath [ qemu_pkg vde2 netpbm coreutils ]}" \
61
62 install -m 0644 -vD driver-exports $out/nix-support/driver-exports
63 '';
64 };
65
66 # Run an automated test suite in the given virtual network.
67 runTests = {
68 # the script that runs the network
69 driver,
70 # a source position in the format of builtins.unsafeGetAttrPos
71 # for meta.position
72 pos,
73 }:
74 stdenv.mkDerivation {
75 name = "vm-test-run-${driver.testName}";
76
77 requiredSystemFeatures = [ "kvm" "nixos-test" ];
78
79 buildCommand =
80 ''
81 mkdir -p $out
82
83 LOGFILE=/dev/null tests='exec(os.environ["testScript"])' ${driver}/bin/nixos-test-driver
84 '';
85
86 passthru = driver.passthru // {
87 inherit driver;
88 };
89
90 inherit pos;
91 };
92
93
94 makeTest =
95 { testScript
96 , enableOCR ? false
97 , name ? "unnamed"
98 # Skip linting (mainly intended for faster dev cycles)
99 , skipLint ? false
100 , passthru ? {}
101 , # For meta.position
102 pos ? # position used in error messages and for meta.position
103 (if t.meta.description or null != null
104 then builtins.unsafeGetAttrPos "description" t.meta
105 else builtins.unsafeGetAttrPos "testScript" t)
106 , ...
107 } @ t:
108 let
109 # A standard store path to the vm monitor is built like this:
110 # /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor
111 # The max filename length of a unix domain socket is 108 bytes.
112 # This means $name can at most be 50 bytes long.
113 maxTestNameLen = 50;
114 testNameLen = builtins.stringLength name;
115
116
117
118 ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; };
119
120 imagemagick_tiff = imagemagick_light.override { inherit libtiff; };
121
122 # Generate convenience wrappers for running the test driver
123 # interactively with the specified network, and for starting the
124 # VMs from the command line.
125 mkDriver = qemu_pkg:
126 let
127 build-vms = import ./build-vms.nix {
128 inherit system pkgs minimal specialArgs;
129 extraConfigurations = extraConfigurations ++ (pkgs.lib.optional (qemu_pkg != null)
130 {
131 virtualisation.qemu.package = qemu_pkg;
132 }
133 );
134 };
135
136 # FIXME: get this pkg from the module system
137 testDriver = mkTestDriver (if qemu_pkg == null then pkgs.qemu_test else qemu_pkg);
138
139 nodes = build-vms.buildVirtualNetwork (
140 t.nodes or (if t ? machine then { machine = t.machine; } else { })
141 );
142 vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
143 vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
144
145 testScript' =
146 # Call the test script with the computed nodes.
147 if lib.isFunction testScript
148 then testScript { inherit nodes; }
149 else testScript;
150
151 testDriverName = with builtins;
152 if testNameLen > maxTestNameLen then
153 abort
154 ("The name of the test '${name}' must not be longer than ${toString maxTestNameLen} " +
155 "it's currently ${toString testNameLen} characters long.")
156 else
157 "nixos-test-driver-${name}";
158 in
159 lib.warnIf skipLint "Linting is disabled" (runCommand testDriverName
160 {
161 nativeBuildInputs = [ makeWrapper ];
162 testScript = testScript';
163 preferLocalBuild = true;
164 testName = name;
165 passthru = passthru // {
166 inherit nodes;
167 };
168 }
169 ''
170 mkdir -p $out/bin
171
172 echo -n "$testScript" > $out/test-script
173 ${lib.optionalString (!skipLint) ''
174 PYFLAKES_BUILTINS="$(
175 echo -n ${lib.escapeShellArg (lib.concatStringsSep "," nodeHostNames)},
176 < ${lib.escapeShellArg "${testDriver}/nix-support/driver-exports"}
177 )" ${python3Packages.pyflakes}/bin/pyflakes $out/test-script
178 ''}
179
180 ln -s ${testDriver}/bin/nixos-test-driver $out/bin/
181 vms=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
182 wrapProgram $out/bin/nixos-test-driver \
183 --add-flags "''${vms[*]}" \
184 ${lib.optionalString enableOCR
185 "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
186 --run "export testScript=\"\$(${coreutils}/bin/cat $out/test-script)\"" \
187 --set VLANS '${toString vlans}'
188 ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
189 wrapProgram $out/bin/nixos-run-vms \
190 --add-flags "''${vms[*]}" \
191 ${lib.optionalString enableOCR "--prefix PATH : '${ocrProg}/bin'"} \
192 --set tests 'start_all(); join_all();' \
193 --set VLANS '${toString vlans}' \
194 ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"}
195 ''); # "
196
197 passMeta = drv: drv // lib.optionalAttrs (t ? meta) {
198 meta = (drv.meta or { }) // t.meta;
199 };
200
201 driver = mkDriver null;
202 driverInteractive = mkDriver pkgs.qemu;
203
204 test = passMeta (runTests { inherit driver pos; });
205
206 nodeNames = builtins.attrNames driver.nodes;
207 invalidNodeNames = lib.filter
208 (node: builtins.match "^[A-z_]([A-z0-9_]+)?$" node == null)
209 nodeNames;
210
211 nodeHostNames = map (c: c.config.system.name) (lib.attrValues driver.nodes);
212
213 in
214 if lib.length invalidNodeNames > 0 then
215 throw ''
216 Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
217 All machines are referenced as python variables in the testing framework which will break the
218 script when special characters are used.
219
220 Please stick to alphanumeric chars and underscores as separation.
221 ''
222 else
223 test // {
224 inherit test driver driverInteractive;
225 inherit (driver) nodes;
226 };
227
228 runInMachine =
229 { drv
230 , machine
231 , preBuild ? ""
232 , postBuild ? ""
233 , qemu ? pkgs.qemu_test
234 , ... # ???
235 }:
236 let
237 build-vms = import ./build-vms.nix {
238 inherit system pkgs minimal specialArgs extraConfigurations;
239 };
240
241 vm = build-vms.buildVM { }
242 [
243 machine
244 {
245 key = "run-in-machine";
246 networking.hostName = "client";
247 nix.readOnlyStore = false;
248 virtualisation.writableStore = false;
249 }
250 ];
251
252 buildrunner = writeText "vm-build" ''
253 source $1
254
255 ${coreutils}/bin/mkdir -p $TMPDIR
256 cd $TMPDIR
257
258 exec $origBuilder $origArgs
259 '';
260
261 testScript = ''
262 start_all()
263 client.wait_for_unit("multi-user.target")
264 ${preBuild}
265 client.succeed("env -i ${bash}/bin/bash ${buildrunner} /tmp/xchg/saved-env >&2")
266 ${postBuild}
267 client.succeed("sync") # flush all data before pulling the plug
268 '';
269
270 vmRunCommand = writeText "vm-run" ''
271 xchg=vm-state-client/xchg
272 ${coreutils}/bin/mkdir $out
273 ${coreutils}/bin/mkdir -p $xchg
274
275 for i in $passAsFile; do
276 i2=''${i}Path
277 _basename=$(${coreutils}/bin/basename ''${!i2})
278 ${coreutils}/bin/cp ''${!i2} $xchg/$_basename
279 eval $i2=/tmp/xchg/$_basename
280 ${coreutils}/bin/ls -la $xchg
281 done
282
283 unset i i2 _basename
284 export | ${gnugrep}/bin/grep -v '^xchg=' > $xchg/saved-env
285 unset xchg
286
287 export tests='${testScript}'
288 ${mkTestDriver qemu}/bin/nixos-test-driver --keep-vm-state ${vm.config.system.build.vm}/bin/run-*-vm
289 ''; # */
290
291 in
292 lib.overrideDerivation drv (attrs: {
293 requiredSystemFeatures = [ "kvm" ];
294 builder = "${bash}/bin/sh";
295 args = [ "-e" vmRunCommand ];
296 origArgs = attrs.args;
297 origBuilder = attrs.builder;
298 });
299
300
301 runInMachineWithX = { require ? [ ], ... } @ args:
302 let
303 client =
304 { ... }:
305 {
306 inherit require;
307 imports = [
308 ../tests/common/auto.nix
309 ];
310 virtualisation.memorySize = 1024;
311 services.xserver.enable = true;
312 test-support.displayManager.auto.enable = true;
313 services.xserver.displayManager.defaultSession = "none+icewm";
314 services.xserver.windowManager.icewm.enable = true;
315 };
316 in
317 runInMachine ({
318 machine = client;
319 preBuild =
320 ''
321 client.wait_for_x()
322 '';
323 } // args);
324
325
326 simpleTest = as: (makeTest as).test;
327
328}