1import ./make-test-python.nix (
2 { pkgs, lib, ... }:
3 {
4 name = "turbovnc-headless-server";
5 meta = {
6 maintainers = with lib.maintainers; [ nh2 ];
7 };
8
9 nodes.machine =
10 { pkgs, ... }:
11 {
12
13 environment.systemPackages = with pkgs; [
14 mesa-demos
15 procps # for `pkill`, `pidof` in the test
16 scrot # for screenshotting Xorg
17 turbovnc
18 ];
19
20 programs.turbovnc.ensureHeadlessSoftwareOpenGL = true;
21
22 networking.firewall = {
23 # Reject instead of drop, for failures instead of hangs.
24 rejectPackets = true;
25 allowedTCPPorts = [
26 5900 # VNC :0, for seeing what's going on in the server
27 ];
28 };
29
30 # So that we can ssh into the VM, see e.g.
31 # https://nixos.org/manual/nixos/stable/#sec-nixos-test-port-forwarding
32 services.openssh.enable = true;
33 users.mutableUsers = false;
34 # `test-instrumentation.nix` already sets an empty root password.
35 # The following have to all be set to allow an empty SSH login password.
36 services.openssh.settings.PermitRootLogin = "yes";
37 services.openssh.settings.PermitEmptyPasswords = "yes";
38 security.pam.services.sshd.allowNullPassword = true; # the default `UsePam yes` makes this necessary
39 };
40
41 testScript = ''
42 def wait_until_terminated_or_succeeds(
43 termination_check_shell_command,
44 success_check_shell_command,
45 get_detail_message_fn,
46 retries=60,
47 retry_sleep=0.5,
48 ):
49 def check_success():
50 command_exit_code, _output = machine.execute(success_check_shell_command)
51 return command_exit_code == 0
52
53 for _ in range(retries):
54 exit_check_exit_code, _output = machine.execute(termination_check_shell_command)
55 is_terminated = exit_check_exit_code != 0
56 if is_terminated:
57 if check_success():
58 return
59 else:
60 details = get_detail_message_fn()
61 raise Exception(
62 f"termination check ({termination_check_shell_command}) triggered without command succeeding ({success_check_shell_command}); details: {details}"
63 )
64 else:
65 if check_success():
66 return
67 import time
68 time.sleep(retry_sleep)
69
70 if not check_success():
71 details = get_detail_message_fn()
72 raise Exception(
73 f"action timed out ({success_check_shell_command}); details: {details}"
74 )
75
76
77 # Below we use the pattern:
78 # (cmd | tee stdout.log) 3>&1 1>&2 2>&3 | tee stderr.log
79 # to capture both stderr and stdout while also teeing them, see:
80 # https://unix.stackexchange.com/questions/6430/how-to-redirect-stderr-and-stdout-to-different-files-and-also-display-in-termina/6431#6431
81
82
83 # Starts headless VNC server, backgrounding it.
84 def start_xvnc():
85 xvnc_command = " ".join(
86 [
87 "Xvnc",
88 ":0",
89 "-iglx",
90 "-auth /root/.Xauthority",
91 "-geometry 1240x900",
92 "-depth 24",
93 "-rfbwait 5000",
94 "-deferupdate 1",
95 "-verbose",
96 "-securitytypes none",
97 # We don't enforce localhost listening such that we
98 # can connect from outside the VM using
99 # env QEMU_NET_OPTS=hostfwd=tcp::5900-:5900 $(nix-build nixos/tests/turbovnc-headless-server.nix -A driver)/bin/nixos-test-driver
100 # for testing purposes, and so that we can in the future
101 # add another test case that connects the TurboVNC client.
102 # "-localhost",
103 ]
104 )
105 machine.execute(
106 # Note trailing & for backgrounding.
107 f"({xvnc_command} | tee /tmp/Xvnc.stdout) 3>&1 1>&2 2>&3 | tee /tmp/Xvnc.stderr >&2 &",
108 )
109
110
111 # Waits until the server log message that tells us that GLX is ready
112 # (requires `-verbose` above), avoiding screenshoting racing below.
113 def wait_until_xvnc_glx_ready():
114 machine.wait_until_succeeds("test -f /tmp/Xvnc.stderr")
115 wait_until_terminated_or_succeeds(
116 termination_check_shell_command="pidof Xvnc",
117 success_check_shell_command="grep 'GLX: Initialized DRISWRAST' /tmp/Xvnc.stderr",
118 get_detail_message_fn=lambda: "Contents of /tmp/Xvnc.stderr:\n"
119 + machine.succeed("cat /tmp/Xvnc.stderr"),
120 )
121
122
123 # Checks that we detect glxgears failing when
124 # `LIBGL_DRIVERS_PATH=/nonexistent` is set
125 # (in which case software rendering should not work).
126 def test_glxgears_failing_with_bad_driver_path():
127 machine.execute(
128 # Note trailing & for backgrounding.
129 "(env DISPLAY=:0 LIBGL_DRIVERS_PATH=/nonexistent glxgears -info | tee /tmp/glxgears-should-fail.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears-should-fail.stderr >&2 &"
130 )
131 machine.wait_until_succeeds("test -f /tmp/glxgears-should-fail.stderr")
132 wait_until_terminated_or_succeeds(
133 termination_check_shell_command="pidof glxgears",
134 success_check_shell_command="grep 'MESA-LOADER: failed to open swrast' /tmp/glxgears-should-fail.stderr",
135 get_detail_message_fn=lambda: "Contents of /tmp/glxgears-should-fail.stderr:\n"
136 + machine.succeed("cat /tmp/glxgears-should-fail.stderr"),
137 )
138 machine.wait_until_fails("pidof glxgears")
139
140
141 # Starts glxgears, backgrounding it. Waits until it prints the `GL_RENDERER`.
142 # Does not quit glxgears.
143 def test_glxgears_prints_renderer():
144 machine.execute(
145 # Note trailing & for backgrounding.
146 "(env DISPLAY=:0 glxgears -info | tee /tmp/glxgears.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears.stderr >&2 &"
147 )
148 machine.wait_until_succeeds("test -f /tmp/glxgears.stderr")
149 wait_until_terminated_or_succeeds(
150 termination_check_shell_command="pidof glxgears",
151 success_check_shell_command="grep 'GL_RENDERER' /tmp/glxgears.stdout",
152 get_detail_message_fn=lambda: "Contents of /tmp/glxgears.stderr:\n"
153 + machine.succeed("cat /tmp/glxgears.stderr"),
154 )
155
156
157 with subtest("Start Xvnc"):
158 start_xvnc()
159 wait_until_xvnc_glx_ready()
160
161 with subtest("Ensure bad driver path makes glxgears fail"):
162 test_glxgears_failing_with_bad_driver_path()
163
164 with subtest("Run 3D application (glxgears)"):
165 test_glxgears_prints_renderer()
166
167 # Take screenshot; should display the glxgears.
168 machine.succeed("scrot --display :0 /tmp/glxgears.png")
169
170 # Copy files down.
171 machine.copy_from_vm("/tmp/glxgears.png")
172 machine.copy_from_vm("/tmp/glxgears.stdout")
173 machine.copy_from_vm("/tmp/glxgears-should-fail.stdout")
174 machine.copy_from_vm("/tmp/glxgears-should-fail.stderr")
175 machine.copy_from_vm("/tmp/Xvnc.stdout")
176 machine.copy_from_vm("/tmp/Xvnc.stderr")
177 '';
178
179 }
180)