1# Terminal emulators all present a pretty similar interface.
2# That gives us an opportunity to easily test their basic functionality with a single codebase.
3#
4# There are two tests run on each terminal emulator
5# - can it successfully execute a command passed on the cmdline?
6# - can it successfully display a colour?
7# the latter is used as a proxy for "can it display text?", without going through all the intricacies of OCR.
8#
9# 256-colour terminal mode is used to display the test colour, since it has a universally-applicable palette (unlike 8- and 16- colour, where the colours are implementation-defined), and it is widely supported (unlike 24-bit colour).
10#
11# Future work:
12# - Wayland support (both for testing the existing terminals, and for testing wayland-only terminals like foot and havoc)
13# - Test keyboard input? (skipped for now, to eliminate the possibility of race conditions and focus issues)
14
15{ system ? builtins.currentSystem,
16 config ? {},
17 pkgs ? import ../.. { inherit system config; }
18}:
19
20with import ../lib/testing-python.nix { inherit system pkgs; };
21with pkgs.lib;
22
23let tests = {
24 alacritty.pkg = p: p.alacritty;
25
26 contour.pkg = p: p.contour;
27 contour.cmd = "contour early-exit-threshold 0 execute $command";
28
29 cool-retro-term.pkg = p: p.cool-retro-term;
30 cool-retro-term.colourTest = false; # broken by gloss effect
31
32 ctx.pkg = p: p.ctx;
33 ctx.pinkValue = "#FE0065";
34
35 darktile.pkg = p: p.darktile;
36
37 deepin-terminal.pkg = p: p.deepin.deepin-terminal;
38
39 eterm.pkg = p: p.eterm;
40 eterm.executable = "Eterm";
41 eterm.pinkValue = "#D40055";
42
43 germinal.pkg = p: p.germinal;
44
45 gnome-terminal.pkg = p: p.gnome.gnome-terminal;
46
47 guake.pkg = p: p.guake;
48 guake.cmd = "SHELL=$command guake --show";
49 guake.kill = true;
50
51 hyper.pkg = p: p.hyper;
52
53 kermit.pkg = p: p.kermit-terminal;
54
55 kgx.pkg = p: p.kgx;
56 kgx.cmd = "kgx -e $command";
57 kgx.kill = true;
58
59 kitty.pkg = p: p.kitty;
60 kitty.cmd = "kitty $command";
61
62 konsole.pkg = p: p.plasma5Packages.konsole;
63
64 lomiri-terminal-app.pkg = p: p.lomiri.lomiri-terminal-app;
65
66 lxterminal.pkg = p: p.lxterminal;
67
68 mate-terminal.pkg = p: p.mate.mate-terminal;
69 mate-terminal.cmd = "SHELL=$command mate-terminal --disable-factory"; # factory mode uses dbus, and we don't have a proper dbus session set up
70
71 mlterm.pkg = p: p.mlterm;
72
73 mrxvt.pkg = p: p.mrxvt;
74
75 qterminal.pkg = p: p.lxqt.qterminal;
76 qterminal.kill = true;
77
78 rio.pkg = p: p.rio;
79 rio.cmd = "rio -e $command";
80 rio.colourTest = false; # the rendering is changing too much so colors change every release.
81
82 roxterm.pkg = p: p.roxterm;
83 roxterm.cmd = "roxterm -e $command";
84
85 sakura.pkg = p: p.sakura;
86
87 st.pkg = p: p.st;
88 st.kill = true;
89
90 stupidterm.pkg = p: p.stupidterm;
91 stupidterm.cmd = "stupidterm -- $command";
92
93 terminator.pkg = p: p.terminator;
94 terminator.cmd = "terminator -e $command";
95
96 terminology.pkg = p: p.enlightenment.terminology;
97 terminology.cmd = "SHELL=$command terminology --no-wizard=true";
98 terminology.colourTest = false; # broken by gloss effect
99
100 termite.pkg = p: p.termite;
101
102 termonad.pkg = p: p.termonad;
103
104 tilda.pkg = p: p.tilda;
105
106 tilix.pkg = p: p.tilix;
107 tilix.cmd = "tilix -e $command";
108
109 urxvt.pkg = p: p.rxvt-unicode;
110
111 wayst.pkg = p: p.wayst;
112 wayst.pinkValue = "#FF0066";
113
114 # times out after spending many hours
115 #wezterm.pkg = p: p.wezterm;
116
117 xfce4-terminal.pkg = p: p.xfce.xfce4-terminal;
118
119 xterm.pkg = p: p.xterm;
120 };
121in mapAttrs (name: { pkg, executable ? name, cmd ? "SHELL=$command ${executable}", colourTest ? true, pinkValue ? "#FF0087", kill ? false }: makeTest
122{
123 name = "terminal-emulator-${name}";
124 meta = with pkgs.lib.maintainers; {
125 maintainers = [ jjjollyjim ];
126 };
127
128 nodes.machine = { pkgsInner, ... }:
129
130 {
131 imports = [ ./common/x11.nix ./common/user-account.nix ];
132
133 # Hyper (and any other electron-based terminals) won't run as root
134 test-support.displayManager.auto.user = "alice";
135
136 environment.systemPackages = [
137 (pkg pkgs)
138 (pkgs.writeShellScriptBin "report-success" ''
139 echo 1 > /tmp/term-ran-successfully
140 ${optionalString kill "pkill ${executable}"}
141 '')
142 (pkgs.writeShellScriptBin "display-colour" ''
143 # A 256-colour background colour code for pink, then spaces.
144 #
145 # Background is used rather than foreground to minimize the effect of anti-aliasing.
146 #
147 # Keep adding more in case the window is partially offscreen to the left or requires
148 # a change to correctly redraw after initialising the window (as with ctx).
149
150 while :
151 do
152 echo -ne "\e[48;5;198m "
153 sleep 0.5
154 done
155 sleep infinity
156 '')
157 (pkgs.writeShellScriptBin "run-in-this-term" "sudo -u alice run-in-this-term-wrapped $1")
158
159 (pkgs.writeShellScriptBin "run-in-this-term-wrapped" "command=\"$(which \"$1\")\"; ${cmd}")
160 ];
161
162 # Helpful reminder to add this test to passthru.tests
163 warnings = if !((pkg pkgs) ? "passthru" && (pkg pkgs).passthru ? "tests") then [ "The package for ${name} doesn't have a passthru.tests" ] else [ ];
164 };
165
166 # We need imagemagick, though not tesseract
167 enableOCR = true;
168
169 testScript = { nodes, ... }: let
170 in ''
171 with subtest("wait for x"):
172 start_all()
173 machine.wait_for_x()
174
175 with subtest("have the terminal run a command"):
176 # We run this command synchronously, so we can be certain the exit codes are happy
177 machine.${if kill then "execute" else "succeed"}("run-in-this-term report-success")
178 machine.wait_for_file("/tmp/term-ran-successfully")
179 ${optionalString colourTest ''
180
181 import tempfile
182 import subprocess
183
184
185 def check_for_pink(final=False) -> bool:
186 with tempfile.NamedTemporaryFile() as tmpin:
187 machine.send_monitor_command("screendump {}".format(tmpin.name))
188
189 cmd = 'convert {} -define histogram:unique-colors=true -format "%c" histogram:info:'.format(
190 tmpin.name
191 )
192 ret = subprocess.run(cmd, shell=True, capture_output=True)
193 if ret.returncode != 0:
194 raise Exception(
195 "image analysis failed with exit code {}".format(ret.returncode)
196 )
197
198 text = ret.stdout.decode("utf-8")
199 return "${pinkValue}" in text
200
201
202 with subtest("ensuring no pink is present without the terminal"):
203 assert (
204 check_for_pink() == False
205 ), "Pink was present on the screen before we even launched a terminal!"
206
207 with subtest("have the terminal display a colour"):
208 # We run this command in the background
209 assert machine.shell is not None
210 machine.shell.send(b"(run-in-this-term display-colour |& systemd-cat -t terminal) &\n")
211
212 with machine.nested("Waiting for the screen to have pink on it:"):
213 retry(check_for_pink)
214 ''}'';
215}
216
217 ) tests