@@ -0,0 +1,526 @@
1+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-writing-nixos-tests">
2+ <title>Writing Tests</title>
3+ <para>
4+ A NixOS test is a Nix expression that has the following structure:
5+ </para>
6+ <programlisting language="bash">
7+import ./make-test-python.nix {
8+
9+ # Either the configuration of a single machine:
10+ machine =
11+ { config, pkgs, ... }:
12+ { configuration…
13+ };
14+
15+ # Or a set of machines:
16+ nodes =
17+ { machine1 =
18+ { config, pkgs, ... }: { … };
19+ machine2 =
20+ { config, pkgs, ... }: { … };
21+ …
22+ };
23+
24+ testScript =
25+ ''
26+ Python code…
27+ '';
28+}
29+</programlisting>
30+ <para>
31+ The attribute <literal>testScript</literal> is a bit of Python code
32+ that executes the test (described below). During the test, it will
33+ start one or more virtual machines, the configuration of which is
34+ described by the attribute <literal>machine</literal> (if you need
35+ only one machine in your test) or by the attribute
36+ <literal>nodes</literal> (if you need multiple machines). For
37+ instance,
38+ <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>
39+ only needs a single machine to test whether users can log in on the
40+ virtual console, whether device ownership is correctly maintained
41+ when switching between consoles, and so on. On the other hand,
42+ <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix"><literal>nfs/simple.nix</literal></link>,
43+ which tests NFS client and server functionality in the Linux kernel
44+ (including whether locks are maintained across server crashes),
45+ requires three machines: a server and two clients.
46+ </para>
47+ <para>
48+ There are a few special NixOS configuration options for test VMs:
49+ </para>
50+ <variablelist>
51+ <varlistentry>
52+ <term>
53+ <literal>virtualisation.memorySize</literal>
54+ </term>
55+ <listitem>
56+ <para>
57+ The memory of the VM in megabytes.
58+ </para>
59+ </listitem>
60+ </varlistentry>
61+ <varlistentry>
62+ <term>
63+ <literal>virtualisation.vlans</literal>
64+ </term>
65+ <listitem>
66+ <para>
67+ The virtual networks to which the VM is connected. See
68+ <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix"><literal>nat.nix</literal></link>
69+ for an example.
70+ </para>
71+ </listitem>
72+ </varlistentry>
73+ <varlistentry>
74+ <term>
75+ <literal>virtualisation.writableStore</literal>
76+ </term>
77+ <listitem>
78+ <para>
79+ By default, the Nix store in the VM is not writable. If you
80+ enable this option, a writable union file system is mounted on
81+ top of the Nix store to make it appear writable. This is
82+ necessary for tests that run Nix operations that modify the
83+ store.
84+ </para>
85+ </listitem>
86+ </varlistentry>
87+ </variablelist>
88+ <para>
89+ For more options, see the module
90+ <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix"><literal>qemu-vm.nix</literal></link>.
91+ </para>
92+ <para>
93+ The test script is a sequence of Python statements that perform
94+ various actions, such as starting VMs, executing commands in the
95+ VMs, and so on. Each virtual machine is represented as an object
96+ stored in the variable <literal>name</literal> if this is also the
97+ identifier of the machine in the declarative config. If you didn't
98+ specify multiple machines using the <literal>nodes</literal>
99+ attribute, it is just <literal>machine</literal>. The following
100+ example starts the machine, waits until it has finished booting,
101+ then executes a command and checks that the output is more-or-less
102+ correct:
103+ </para>
104+ <programlisting language="python">
105+machine.start()
106+machine.wait_for_unit("default.target")
107+if not "Linux" in machine.succeed("uname"):
108+ raise Exception("Wrong OS")
109+</programlisting>
110+ <para>
111+ The first line is actually unnecessary; machines are implicitly
112+ started when you first execute an action on them (such as
113+ <literal>wait_for_unit</literal> or <literal>succeed</literal>). If
114+ you have multiple machines, you can speed up the test by starting
115+ them in parallel:
116+ </para>
117+ <programlisting language="python">
118+start_all()
119+</programlisting>
120+ <para>
121+ The following methods are available on machine objects:
122+ </para>
123+ <variablelist>
124+ <varlistentry>
125+ <term>
126+ <literal>start</literal>
127+ </term>
128+ <listitem>
129+ <para>
130+ Start the virtual machine. This method is asynchronous — it
131+ does not wait for the machine to finish booting.
132+ </para>
133+ </listitem>
134+ </varlistentry>
135+ <varlistentry>
136+ <term>
137+ <literal>shutdown</literal>
138+ </term>
139+ <listitem>
140+ <para>
141+ Shut down the machine, waiting for the VM to exit.
142+ </para>
143+ </listitem>
144+ </varlistentry>
145+ <varlistentry>
146+ <term>
147+ <literal>crash</literal>
148+ </term>
149+ <listitem>
150+ <para>
151+ Simulate a sudden power failure, by telling the VM to exit
152+ immediately.
153+ </para>
154+ </listitem>
155+ </varlistentry>
156+ <varlistentry>
157+ <term>
158+ <literal>block</literal>
159+ </term>
160+ <listitem>
161+ <para>
162+ Simulate unplugging the Ethernet cable that connects the
163+ machine to the other machines.
164+ </para>
165+ </listitem>
166+ </varlistentry>
167+ <varlistentry>
168+ <term>
169+ <literal>unblock</literal>
170+ </term>
171+ <listitem>
172+ <para>
173+ Undo the effect of <literal>block</literal>.
174+ </para>
175+ </listitem>
176+ </varlistentry>
177+ <varlistentry>
178+ <term>
179+ <literal>screenshot</literal>
180+ </term>
181+ <listitem>
182+ <para>
183+ Take a picture of the display of the virtual machine, in PNG
184+ format. The screenshot is linked from the HTML log.
185+ </para>
186+ </listitem>
187+ </varlistentry>
188+ <varlistentry>
189+ <term>
190+ <literal>get_screen_text_variants</literal>
191+ </term>
192+ <listitem>
193+ <para>
194+ Return a list of different interpretations of what is
195+ currently visible on the machine's screen using optical
196+ character recognition. The number and order of the
197+ interpretations is not specified and is subject to change, but
198+ if no exception is raised at least one will be returned.
199+ </para>
200+ <note>
201+ <para>
202+ This requires passing <literal>enableOCR</literal> to the
203+ test attribute set.
204+ </para>
205+ </note>
206+ </listitem>
207+ </varlistentry>
208+ <varlistentry>
209+ <term>
210+ <literal>get_screen_text</literal>
211+ </term>
212+ <listitem>
213+ <para>
214+ Return a textual representation of what is currently visible
215+ on the machine's screen using optical character recognition.
216+ </para>
217+ <note>
218+ <para>
219+ This requires passing <literal>enableOCR</literal> to the
220+ test attribute set.
221+ </para>
222+ </note>
223+ </listitem>
224+ </varlistentry>
225+ <varlistentry>
226+ <term>
227+ <literal>send_monitor_command</literal>
228+ </term>
229+ <listitem>
230+ <para>
231+ Send a command to the QEMU monitor. This is rarely used, but
232+ allows doing stuff such as attaching virtual USB disks to a
233+ running machine.
234+ </para>
235+ </listitem>
236+ </varlistentry>
237+ <varlistentry>
238+ <term>
239+ <literal>send_key</literal>
240+ </term>
241+ <listitem>
242+ <para>
243+ Simulate pressing keys on the virtual keyboard, e.g.,
244+ <literal>send_key("ctrl-alt-delete")</literal>.
245+ </para>
246+ </listitem>
247+ </varlistentry>
248+ <varlistentry>
249+ <term>
250+ <literal>send_chars</literal>
251+ </term>
252+ <listitem>
253+ <para>
254+ Simulate typing a sequence of characters on the virtual
255+ keyboard, e.g.,
256+ <literal>send_chars("foobar\n")</literal> will type
257+ the string <literal>foobar</literal> followed by the Enter
258+ key.
259+ </para>
260+ </listitem>
261+ </varlistentry>
262+ <varlistentry>
263+ <term>
264+ <literal>execute</literal>
265+ </term>
266+ <listitem>
267+ <para>
268+ Execute a shell command, returning a list
269+ <literal>(status, stdout)</literal>.
270+ </para>
271+ </listitem>
272+ </varlistentry>
273+ <varlistentry>
274+ <term>
275+ <literal>succeed</literal>
276+ </term>
277+ <listitem>
278+ <para>
279+ Execute a shell command, raising an exception if the exit
280+ status is not zero, otherwise returning the standard output.
281+ Commands are run with <literal>set -euo pipefail</literal>
282+ set:
283+ </para>
284+ <itemizedlist>
285+ <listitem>
286+ <para>
287+ If several commands are separated by <literal>;</literal>
288+ and one fails, the command as a whole will fail.
289+ </para>
290+ </listitem>
291+ <listitem>
292+ <para>
293+ For pipelines, the last non-zero exit status will be
294+ returned (if there is one, zero will be returned
295+ otherwise).
296+ </para>
297+ </listitem>
298+ <listitem>
299+ <para>
300+ Dereferencing unset variables fail the command.
301+ </para>
302+ </listitem>
303+ </itemizedlist>
304+ </listitem>
305+ </varlistentry>
306+ <varlistentry>
307+ <term>
308+ <literal>fail</literal>
309+ </term>
310+ <listitem>
311+ <para>
312+ Like <literal>succeed</literal>, but raising an exception if
313+ the command returns a zero status.
314+ </para>
315+ </listitem>
316+ </varlistentry>
317+ <varlistentry>
318+ <term>
319+ <literal>wait_until_succeeds</literal>
320+ </term>
321+ <listitem>
322+ <para>
323+ Repeat a shell command with 1-second intervals until it
324+ succeeds.
325+ </para>
326+ </listitem>
327+ </varlistentry>
328+ <varlistentry>
329+ <term>
330+ <literal>wait_until_fails</literal>
331+ </term>
332+ <listitem>
333+ <para>
334+ Repeat a shell command with 1-second intervals until it fails.
335+ </para>
336+ </listitem>
337+ </varlistentry>
338+ <varlistentry>
339+ <term>
340+ <literal>wait_for_unit</literal>
341+ </term>
342+ <listitem>
343+ <para>
344+ Wait until the specified systemd unit has reached the
345+ <quote>active</quote> state.
346+ </para>
347+ </listitem>
348+ </varlistentry>
349+ <varlistentry>
350+ <term>
351+ <literal>wait_for_file</literal>
352+ </term>
353+ <listitem>
354+ <para>
355+ Wait until the specified file exists.
356+ </para>
357+ </listitem>
358+ </varlistentry>
359+ <varlistentry>
360+ <term>
361+ <literal>wait_for_open_port</literal>
362+ </term>
363+ <listitem>
364+ <para>
365+ Wait until a process is listening on the given TCP port (on
366+ <literal>localhost</literal>, at least).
367+ </para>
368+ </listitem>
369+ </varlistentry>
370+ <varlistentry>
371+ <term>
372+ <literal>wait_for_closed_port</literal>
373+ </term>
374+ <listitem>
375+ <para>
376+ Wait until nobody is listening on the given TCP port.
377+ </para>
378+ </listitem>
379+ </varlistentry>
380+ <varlistentry>
381+ <term>
382+ <literal>wait_for_x</literal>
383+ </term>
384+ <listitem>
385+ <para>
386+ Wait until the X11 server is accepting connections.
387+ </para>
388+ </listitem>
389+ </varlistentry>
390+ <varlistentry>
391+ <term>
392+ <literal>wait_for_text</literal>
393+ </term>
394+ <listitem>
395+ <para>
396+ Wait until the supplied regular expressions matches the
397+ textual contents of the screen by using optical character
398+ recognition (see <literal>get_screen_text</literal> and
399+ <literal>get_screen_text_variants</literal>).
400+ </para>
401+ <note>
402+ <para>
403+ This requires passing <literal>enableOCR</literal> to the
404+ test attribute set.
405+ </para>
406+ </note>
407+ </listitem>
408+ </varlistentry>
409+ <varlistentry>
410+ <term>
411+ <literal>wait_for_console_text</literal>
412+ </term>
413+ <listitem>
414+ <para>
415+ Wait until the supplied regular expressions match a line of
416+ the serial console output. This method is useful when OCR is
417+ not possibile or accurate enough.
418+ </para>
419+ </listitem>
420+ </varlistentry>
421+ <varlistentry>
422+ <term>
423+ <literal>wait_for_window</literal>
424+ </term>
425+ <listitem>
426+ <para>
427+ Wait until an X11 window has appeared whose name matches the
428+ given regular expression, e.g.,
429+ <literal>wait_for_window("Terminal")</literal>.
430+ </para>
431+ </listitem>
432+ </varlistentry>
433+ <varlistentry>
434+ <term>
435+ <literal>copy_from_host</literal>
436+ </term>
437+ <listitem>
438+ <para>
439+ Copies a file from host to machine, e.g.,
440+ <literal>copy_from_host("myfile", "/etc/my/important/file")</literal>.
441+ </para>
442+ <para>
443+ The first argument is the file on the host. The file needs to
444+ be accessible while building the nix derivation. The second
445+ argument is the location of the file on the machine.
446+ </para>
447+ </listitem>
448+ </varlistentry>
449+ <varlistentry>
450+ <term>
451+ <literal>systemctl</literal>
452+ </term>
453+ <listitem>
454+ <para>
455+ Runs <literal>systemctl</literal> commands with optional
456+ support for <literal>systemctl --user</literal>
457+ </para>
458+ <programlisting language="python">
459+machine.systemctl("list-jobs --no-pager") # runs `systemctl list-jobs --no-pager`
460+machine.systemctl("list-jobs --no-pager", "any-user") # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager`
461+</programlisting>
462+ </listitem>
463+ </varlistentry>
464+ <varlistentry>
465+ <term>
466+ <literal>shell_interact</literal>
467+ </term>
468+ <listitem>
469+ <para>
470+ Allows you to directly interact with the guest shell. This
471+ should only be used during test development, not in production
472+ tests. Killing the interactive session with
473+ <literal>Ctrl-d</literal> or <literal>Ctrl-c</literal> also
474+ ends the guest session.
475+ </para>
476+ </listitem>
477+ </varlistentry>
478+ </variablelist>
479+ <para>
480+ To test user units declared by
481+ <literal>systemd.user.services</literal> the optional
482+ <literal>user</literal> argument can be used:
483+ </para>
484+ <programlisting language="python">
485+machine.start()
486+machine.wait_for_x()
487+machine.wait_for_unit("xautolock.service", "x-session-user")
488+</programlisting>
489+ <para>
490+ This applies to <literal>systemctl</literal>,
491+ <literal>get_unit_info</literal>, <literal>wait_for_unit</literal>,
492+ <literal>start_job</literal> and <literal>stop_job</literal>.
493+ </para>
494+ <para>
495+ For faster dev cycles it's also possible to disable the code-linters
496+ (this shouldn't be commited though):
497+ </para>
498+ <programlisting language="bash">
499+import ./make-test-python.nix {
500+ skipLint = true;
501+ machine =
502+ { config, pkgs, ... }:
503+ { configuration…
504+ };
505+
506+ testScript =
507+ ''
508+ Python code…
509+ '';
510+}
511+</programlisting>
512+ <para>
513+ This will produce a Nix warning at evaluation time. To fully disable
514+ the linter, wrap the test script in comment directives to disable
515+ the Black linter directly (again, don't commit this within the
516+ Nixpkgs repository):
517+ </para>
518+ <programlisting language="bash">
519+ testScript =
520+ ''
521+ # fmt: off
522+ Python code…
523+ # fmt: on
524+ '';
525+</programlisting>
526+</section>