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