summary refs log tree commit diff
path: root/nixos/tests/turbovnc-headless-server.nix
blob: 7d705c56ecf31e0ea0a38e9cdef00a4b3064f433 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import ./make-test-python.nix ({ pkgs, lib, ... }: {
  name = "turbovnc-headless-server";
  meta = {
    maintainers = with lib.maintainers; [ nh2 ];
  };

  machine = { pkgs, ... }: {

    environment.systemPackages = with pkgs; [
      glxinfo
      procps # for `pkill`, `pidof` in the test
      scrot # for screenshotting Xorg
      turbovnc
    ];

    programs.turbovnc.ensureHeadlessSoftwareOpenGL = true;

    networking.firewall = {
      # Reject instead of drop, for failures instead of hangs.
      rejectPackets = true;
      allowedTCPPorts = [
        5900 # VNC :0, for seeing what's going on in the server
      ];
    };

    # So that we can ssh into the VM, see e.g.
    # http://blog.patapon.info/nixos-local-vm/#accessing-the-vm-with-ssh
    services.openssh.enable = true;
    services.openssh.permitRootLogin = "yes";
    users.extraUsers.root.password = "";
    users.mutableUsers = false;
  };

  testScript = ''
    def wait_until_terminated_or_succeeds(
        termination_check_shell_command,
        success_check_shell_command,
        get_detail_message_fn,
        retries=60,
        retry_sleep=0.5,
    ):
        def check_success():
            command_exit_code, _output = machine.execute(success_check_shell_command)
            return command_exit_code == 0

        for _ in range(retries):
            exit_check_exit_code, _output = machine.execute(termination_check_shell_command)
            is_terminated = exit_check_exit_code != 0
            if is_terminated:
                if check_success():
                    return
                else:
                    details = get_detail_message_fn()
                    raise Exception(
                        f"termination check ({termination_check_shell_command}) triggered without command succeeding ({success_check_shell_command}); details: {details}"
                    )
            else:
                if check_success():
                    return
            import time
            time.sleep(retry_sleep)

        if not check_success():
            details = get_detail_message_fn()
            raise Exception(
                f"action timed out ({success_check_shell_command}); details: {details}"
            )


    # Below we use the pattern:
    #     (cmd | tee stdout.log) 3>&1 1>&2 2>&3 | tee stderr.log
    # to capture both stderr and stdout while also teeing them, see:
    # https://unix.stackexchange.com/questions/6430/how-to-redirect-stderr-and-stdout-to-different-files-and-also-display-in-termina/6431#6431


    # Starts headless VNC server, backgrounding it.
    def start_xvnc():
        xvnc_command = " ".join(
            [
                "Xvnc",
                ":0",
                "-iglx",
                "-auth /root/.Xauthority",
                "-geometry 1240x900",
                "-depth 24",
                "-rfbwait 5000",
                "-deferupdate 1",
                "-verbose",
                "-securitytypes none",
                # We don't enforce localhost listening such that we
                # can connect from outside the VM using
                #     env QEMU_NET_OPTS=hostfwd=tcp::5900-:5900 $(nix-build nixos/tests/turbovnc-headless-server.nix -A driver)/bin/nixos-test-driver
                # for testing purposes, and so that we can in the future
                # add another test case that connects the TurboVNC client.
                # "-localhost",
            ]
        )
        machine.execute(
            # Note trailing & for backgrounding.
            f"({xvnc_command} | tee /tmp/Xvnc.stdout) 3>&1 1>&2 2>&3 | tee /tmp/Xvnc.stderr >&2 &",
        )


    # Waits until the server log message that tells us that GLX is ready
    # (requires `-verbose` above), avoiding screenshoting racing below.
    def wait_until_xvnc_glx_ready():
        machine.wait_until_succeeds("test -f /tmp/Xvnc.stderr")
        wait_until_terminated_or_succeeds(
            termination_check_shell_command="pidof Xvnc",
            success_check_shell_command="grep 'GLX: Initialized DRISWRAST' /tmp/Xvnc.stderr",
            get_detail_message_fn=lambda: "Contents of /tmp/Xvnc.stderr:\n"
            + machine.succeed("cat /tmp/Xvnc.stderr"),
        )


    # Checks that we detect glxgears failing when
    # `LIBGL_DRIVERS_PATH=/nonexistent` is set
    # (in which case software rendering should not work).
    def test_glxgears_failing_with_bad_driver_path():
        machine.execute(
            # Note trailing & for backgrounding.
            "(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 &"
        )
        machine.wait_until_succeeds("test -f /tmp/glxgears-should-fail.stderr")
        wait_until_terminated_or_succeeds(
            termination_check_shell_command="pidof glxgears",
            success_check_shell_command="grep 'libGL error: failed to load driver: swrast' /tmp/glxgears-should-fail.stderr",
            get_detail_message_fn=lambda: "Contents of /tmp/glxgears-should-fail.stderr:\n"
            + machine.succeed("cat /tmp/glxgears-should-fail.stderr"),
        )
        machine.wait_until_fails("pidof glxgears")


    # Starts glxgears, backgrounding it. Waits until it prints the `GL_RENDERER`.
    # Does not quit glxgears.
    def test_glxgears_prints_renderer():
        machine.execute(
            # Note trailing & for backgrounding.
            "(env DISPLAY=:0 glxgears -info | tee /tmp/glxgears.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears.stderr >&2 &"
        )
        machine.wait_until_succeeds("test -f /tmp/glxgears.stderr")
        wait_until_terminated_or_succeeds(
            termination_check_shell_command="pidof glxgears",
            success_check_shell_command="grep 'GL_RENDERER' /tmp/glxgears.stdout",
            get_detail_message_fn=lambda: "Contents of /tmp/glxgears.stderr:\n"
            + machine.succeed("cat /tmp/glxgears.stderr"),
        )


    with subtest("Start Xvnc"):
        start_xvnc()
        wait_until_xvnc_glx_ready()

    with subtest("Ensure bad driver path makes glxgears fail"):
        test_glxgears_failing_with_bad_driver_path()

    with subtest("Run 3D application (glxgears)"):
        test_glxgears_prints_renderer()

        # Take screenshot; should display the glxgears.
        machine.succeed("scrot --display :0 /tmp/glxgears.png")

    # Copy files down.
    machine.copy_from_vm("/tmp/glxgears.png")
    machine.copy_from_vm("/tmp/glxgears.stdout")
    machine.copy_from_vm("/tmp/glxgears-should-fail.stdout")
    machine.copy_from_vm("/tmp/glxgears-should-fail.stderr")
    machine.copy_from_vm("/tmp/Xvnc.stdout")
    machine.copy_from_vm("/tmp/Xvnc.stderr")
  '';

})