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