1 import ./make-test-python.nix ({ pkgs, lib, ... }: {
2 name = "turbovnc-headless-server";
4 maintainers = with lib.maintainers; [ nh2 ];
7 nodes.machine = { pkgs, ... }: {
9 environment.systemPackages = with pkgs; [
11 procps # for `pkill`, `pidof` in the test
12 scrot # for screenshotting Xorg
16 programs.turbovnc.ensureHeadlessSoftwareOpenGL = true;
18 networking.firewall = {
19 # Reject instead of drop, for failures instead of hangs.
22 5900 # VNC :0, for seeing what's going on in the server
26 # So that we can ssh into the VM, see e.g.
27 # https://nixos.org/manual/nixos/stable/#sec-nixos-test-port-forwarding
28 services.openssh.enable = true;
29 users.mutableUsers = false;
30 # `test-instrumentation.nix` already sets an empty root password.
31 # The following have to all be set to allow an empty SSH login password.
32 services.openssh.settings.PermitRootLogin = "yes";
33 services.openssh.settings.PermitEmptyPasswords = "yes";
34 security.pam.services.sshd.allowNullPassword = true; # the default `UsePam yes` makes this necessary
38 def wait_until_terminated_or_succeeds(
39 termination_check_shell_command,
40 success_check_shell_command,
41 get_detail_message_fn,
46 command_exit_code, _output = machine.execute(success_check_shell_command)
47 return command_exit_code == 0
49 for _ in range(retries):
50 exit_check_exit_code, _output = machine.execute(termination_check_shell_command)
51 is_terminated = exit_check_exit_code != 0
56 details = get_detail_message_fn()
58 f"termination check ({termination_check_shell_command}) triggered without command succeeding ({success_check_shell_command}); details: {details}"
64 time.sleep(retry_sleep)
66 if not check_success():
67 details = get_detail_message_fn()
69 f"action timed out ({success_check_shell_command}); details: {details}"
73 # Below we use the pattern:
74 # (cmd | tee stdout.log) 3>&1 1>&2 2>&3 | tee stderr.log
75 # to capture both stderr and stdout while also teeing them, see:
76 # https://unix.stackexchange.com/questions/6430/how-to-redirect-stderr-and-stdout-to-different-files-and-also-display-in-termina/6431#6431
79 # Starts headless VNC server, backgrounding it.
81 xvnc_command = " ".join(
86 "-auth /root/.Xauthority",
92 "-securitytypes none",
93 # We don't enforce localhost listening such that we
94 # can connect from outside the VM using
95 # env QEMU_NET_OPTS=hostfwd=tcp::5900-:5900 $(nix-build nixos/tests/turbovnc-headless-server.nix -A driver)/bin/nixos-test-driver
96 # for testing purposes, and so that we can in the future
97 # add another test case that connects the TurboVNC client.
102 # Note trailing & for backgrounding.
103 f"({xvnc_command} | tee /tmp/Xvnc.stdout) 3>&1 1>&2 2>&3 | tee /tmp/Xvnc.stderr >&2 &",
107 # Waits until the server log message that tells us that GLX is ready
108 # (requires `-verbose` above), avoiding screenshoting racing below.
109 def wait_until_xvnc_glx_ready():
110 machine.wait_until_succeeds("test -f /tmp/Xvnc.stderr")
111 wait_until_terminated_or_succeeds(
112 termination_check_shell_command="pidof Xvnc",
113 success_check_shell_command="grep 'GLX: Initialized DRISWRAST' /tmp/Xvnc.stderr",
114 get_detail_message_fn=lambda: "Contents of /tmp/Xvnc.stderr:\n"
115 + machine.succeed("cat /tmp/Xvnc.stderr"),
119 # Checks that we detect glxgears failing when
120 # `LIBGL_DRIVERS_PATH=/nonexistent` is set
121 # (in which case software rendering should not work).
122 def test_glxgears_failing_with_bad_driver_path():
124 # Note trailing & for backgrounding.
125 "(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 &"
127 machine.wait_until_succeeds("test -f /tmp/glxgears-should-fail.stderr")
128 wait_until_terminated_or_succeeds(
129 termination_check_shell_command="pidof glxgears",
130 success_check_shell_command="grep 'MESA-LOADER: failed to open swrast' /tmp/glxgears-should-fail.stderr",
131 get_detail_message_fn=lambda: "Contents of /tmp/glxgears-should-fail.stderr:\n"
132 + machine.succeed("cat /tmp/glxgears-should-fail.stderr"),
134 machine.wait_until_fails("pidof glxgears")
137 # Starts glxgears, backgrounding it. Waits until it prints the `GL_RENDERER`.
138 # Does not quit glxgears.
139 def test_glxgears_prints_renderer():
141 # Note trailing & for backgrounding.
142 "(env DISPLAY=:0 glxgears -info | tee /tmp/glxgears.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears.stderr >&2 &"
144 machine.wait_until_succeeds("test -f /tmp/glxgears.stderr")
145 wait_until_terminated_or_succeeds(
146 termination_check_shell_command="pidof glxgears",
147 success_check_shell_command="grep 'GL_RENDERER' /tmp/glxgears.stdout",
148 get_detail_message_fn=lambda: "Contents of /tmp/glxgears.stderr:\n"
149 + machine.succeed("cat /tmp/glxgears.stderr"),
153 with subtest("Start Xvnc"):
155 wait_until_xvnc_glx_ready()
157 with subtest("Ensure bad driver path makes glxgears fail"):
158 test_glxgears_failing_with_bad_driver_path()
160 with subtest("Run 3D application (glxgears)"):
161 test_glxgears_prints_renderer()
163 # Take screenshot; should display the glxgears.
164 machine.succeed("scrot --display :0 /tmp/glxgears.png")
167 machine.copy_from_vm("/tmp/glxgears.png")
168 machine.copy_from_vm("/tmp/glxgears.stdout")
169 machine.copy_from_vm("/tmp/glxgears-should-fail.stdout")
170 machine.copy_from_vm("/tmp/glxgears-should-fail.stderr")
171 machine.copy_from_vm("/tmp/Xvnc.stdout")
172 machine.copy_from_vm("/tmp/Xvnc.stderr")