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