# Terminal emulators all present a pretty similar interface. # That gives us an opportunity to easily test their basic functionality with a single codebase. # # There are two tests run on each terminal emulator # - can it successfully execute a command passed on the cmdline? # - can it successfully display a colour? # the latter is used as a proxy for "can it display text?", without going through all the intricacies of OCR. # # 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). # # Future work: # - Wayland support (both for testing the existing terminals, and for testing wayland-only terminals like foot and havoc) # - Test keyboard input? (skipped for now, to eliminate the possibility of race conditions and focus issues) { system ? builtins.currentSystem, config ? {}, pkgs ? import ../.. { inherit system config; } }: with import ../lib/testing-python.nix { inherit system pkgs; }; with pkgs.lib; let tests = { alacritty.pkg = p: p.alacritty; contour.pkg = p: p.contour; contour.cmd = "contour $command"; cool-retro-term.pkg = p: p.cool-retro-term; cool-retro-term.colourTest = false; # broken by gloss effect ctx.pkg = p: p.ctx; ctx.pinkValue = "#FE0065"; darktile.pkg = p: p.darktile; eterm.pkg = p: p.eterm; eterm.executable = "Eterm"; eterm.pinkValue = "#D40055"; germinal.pkg = p: p.germinal; gnome-terminal.pkg = p: p.gnome.gnome-terminal; guake.pkg = p: p.guake; guake.cmd = "SHELL=$command guake --show"; guake.kill = true; hyper.pkg = p: p.hyper; kermit.pkg = p: p.kermit-terminal; kgx.pkg = p: p.kgx; kgx.cmd = "kgx -e $command"; kgx.kill = true; kitty.pkg = p: p.kitty; kitty.cmd = "kitty $command"; konsole.pkg = p: p.plasma5Packages.konsole; lxterminal.pkg = p: p.lxterminal; mate-terminal.pkg = p: p.mate.mate-terminal; mate-terminal.cmd = "SHELL=$command mate-terminal --disable-factory"; # factory mode uses dbus, and we don't have a proper dbus session set up mlterm.pkg = p: p.mlterm; mrxvt.pkg = p: p.mrxvt; qterminal.pkg = p: p.lxqt.qterminal; qterminal.kill = true; roxterm.pkg = p: p.roxterm; roxterm.cmd = "roxterm -e $command"; sakura.pkg = p: p.sakura; st.pkg = p: p.st; st.kill = true; stupidterm.pkg = p: p.stupidterm; stupidterm.cmd = "stupidterm -- $command"; terminator.pkg = p: p.terminator; terminator.cmd = "terminator -e $command"; terminology.pkg = p: p.enlightenment.terminology; terminology.cmd = "SHELL=$command terminology --no-wizard=true"; terminology.colourTest = false; # broken by gloss effect termite.pkg = p: p.termite; termonad.pkg = p: p.termonad; tilda.pkg = p: p.tilda; tilix.pkg = p: p.tilix; tilix.cmd = "tilix -e $command"; urxvt.pkg = p: p.rxvt-unicode; wayst.pkg = p: p.wayst; wayst.pinkValue = "#FF0066"; wezterm.pkg = p: p.wezterm; xfce4-terminal.pkg = p: p.xfce.xfce4-terminal; xterm.pkg = p: p.xterm; }; in mapAttrs (name: { pkg, executable ? name, cmd ? "SHELL=$command ${executable}", colourTest ? true, pinkValue ? "#FF0087", kill ? false }: makeTest { name = "terminal-emulator-${name}"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ jjjollyjim ]; }; machine = { pkgsInner, ... }: { imports = [ ./common/x11.nix ./common/user-account.nix ]; # Hyper (and any other electron-based terminals) won't run as root test-support.displayManager.auto.user = "alice"; environment.systemPackages = [ (pkg pkgs) (pkgs.writeShellScriptBin "report-success" '' echo 1 > /tmp/term-ran-successfully ${optionalString kill "pkill ${executable}"} '') (pkgs.writeShellScriptBin "display-colour" '' # A 256-colour background colour code for pink, then spaces. # # Background is used rather than foreground to minimize the effect of anti-aliasing. # # Keep adding more in case the window is partially offscreen to the left or requires # a change to correctly redraw after initialising the window (as with ctx). while : do echo -ne "\e[48;5;198m " sleep 0.5 done sleep infinity '') (pkgs.writeShellScriptBin "run-in-this-term" "sudo -u alice run-in-this-term-wrapped $1") (pkgs.writeShellScriptBin "run-in-this-term-wrapped" "command=\"$(which \"$1\")\"; ${cmd}") ]; # Helpful reminder to add this test to passthru.tests warnings = if !((pkg pkgs) ? "passthru" && (pkg pkgs).passthru ? "tests") then [ "The package for ${name} doesn't have a passthru.tests" ] else [ ]; }; # We need imagemagick, though not tesseract enableOCR = true; testScript = { nodes, ... }: let in '' with subtest("wait for x"): start_all() machine.wait_for_x() with subtest("have the terminal run a command"): # We run this command synchronously, so we can be certain the exit codes are happy machine.${if kill then "execute" else "succeed"}("run-in-this-term report-success") machine.wait_for_file("/tmp/term-ran-successfully") ${optionalString colourTest '' import tempfile import subprocess def check_for_pink(final=False) -> bool: with tempfile.NamedTemporaryFile() as tmpin: machine.send_monitor_command("screendump {}".format(tmpin.name)) cmd = 'convert {} -define histogram:unique-colors=true -format "%c" histogram:info:'.format( tmpin.name ) ret = subprocess.run(cmd, shell=True, capture_output=True) if ret.returncode != 0: raise Exception( "image analysis failed with exit code {}".format(ret.returncode) ) text = ret.stdout.decode("utf-8") return "${pinkValue}" in text with subtest("ensuring no pink is present without the terminal"): assert ( check_for_pink() == False ), "Pink was present on the screen before we even launched a terminal!" with subtest("have the terminal display a colour"): # We run this command in the background machine.shell.send(b"(run-in-this-term display-colour |& systemd-cat -t terminal) &\n") with machine.nested("Waiting for the screen to have pink on it:"): retry(check_for_pink) ''}''; } ) tests