Testing desktop notifications with NixOS

l0b01 pts0 comments

Testing desktop notifications with NixOS | Paperless

Nix

testing

Desktop notifications are a nice way to be informed immediately about important<br>changes on your system. In a terminal or interactive shell script this is easy<br>enough: trap 'notify-send "💥"' ERR. But in a non-interactive or restricted<br>context, such as a systemd service, we need more setup for desktop notifications<br>to work. And with more setup comes more chances to mess up, so it would be good<br>to test that this actually works on a real desktop. Enter NixOS tests!

Nix makes it easy to write reproducible tests.

The tests run in a virtual machine1, so we don’t need to worry about<br>how the host OS is configured.

The test framework supports easily starting services and waiting for them to<br>reach a desired state.

Tests can run anywhere with a bit of RAM and disk space.

At minimum, we need the test to do the following:

Create a NixOS VM with some configuration.

Start the VM.

Run the commands to test the NixOS configuration.

The<br>result<br>is pretty simple (annotated and minimised):

{ pkgs }:<br>let<br>machineName = "machine";<br>in<br>pkgs.testers.runNixOSTest {<br>name = "ssh-server-audit";

nodes.${machineName} = {<br># Import only the NixOS configuration relevant for this test<br>imports = [ ../ssh-server.nix ];

# Optional/irrelevant config omitted<br>};

testScript = ''<br>${machineName}.start()

# Wait for SSH daemon service to start<br>${machineName}.wait_for_unit("sshd.service")

# Should pass SSH server audit<br>${machineName}.succeed("${pkgs.ssh-audit}/bin/ssh-audit 127.0.0.1")<br>'';

This test works great, so we can reuse the general format:

{ pkgs }:<br>let<br>machineName = "machine";<br>in<br>pkgs.testers.runNixOSTest {<br>name = "some-name";

nodes.${machineName} = {<br># NixOS configuration<br>};

testScript = ''<br>${machineName}.start()

${machineName}.succeed("some command")<br>'';

Desktop notification tests need several additional features, and some of these<br>are non-trivial to implement or find examples of. The rest of the article<br>explains how this was achieved. See<br>the final result<br>for the full code.

A functioning graphical desktop

This is available in the existing<br>nixpkgs GNOME tests.<br>I already use GNOME, so reusing this was an obvious choice. The relevant parts<br>of the test are in nodes.${machineName}:

services = {<br>desktopManager.gnome.enable = true;<br>xserver.enable = true;<br>};

Auto-login to the desktop

We need to be logged in to the desktop so that we actually see the<br>notifications. The relevant parts of the test, also based on the GNOME tests<br>above, are in nodes.${machineName}:

let<br>userName = "alice";<br>in<br>services.displayManager.autoLogin = {<br>enable = true;<br>user = userName;<br>};

users.users.${userName} = {<br>isNormalUser = true;<br>uid = 1000; # Used later<br>};

Wait for the desktop to be fully ready for interaction

This functionality is also found in the GNOME tests, but it’s a big chunk of<br>code:

{ pkgs }:<br>let<br>machineName = "machine";<br>userName = "alice";<br>in<br>pkgs.testers.runNixOSTest {<br>nodes.${machineName} = {<br># Allow Eval API access; see<br># https://github.com/NixOS/nixpkgs/blob/07700a890da3eaf57033bc408937e8955ca1c393/nixos/tests/gnome.nix#L25-L38<br>systemd.user.services."org.gnome.Shell@".serviceConfig.ExecStart = [<br>""<br>"${pkgs.gnome-shell}/bin/gnome-shell --unsafe-mode"<br>];<br>};

testScript =<br>{ nodes, ... }:<br>let<br>user = nodes.${machineName}.users.users.${userName};<br>inherit (user) name;<br>bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus";<br>in<br>''<br>start_all()

# Wait for GNOME desktop. Correct output should be (true, 'false')<br>${machineName}.wait_until_succeeds(<br>"su -c '${bus} gdbus call --session --dest=org.gnome.Shell --object-path=/org/gnome/Shell --method=org.gnome.Shell.Eval Main.layoutManager._startingUp' ${name} | grep --fixed-strings --line-regexp --quiet \"(true, 'false')\""<br>'';

Open a GUI where the notification shows permanently

Notifications often disappear after showing for a few seconds, and we want to<br>guarantee that we don’t miss it! This can be done by<br>sending the<br>default key to open the notifications menu,<br>[Super](https://en.wikibooks.org/wiki/QEMU/Monitor#sendkey_keys)-v.<br>We also want to clear any existing notifications. Thus:

{ pkgs }:<br>let<br>machineName = "machine";<br>in<br>pkgs.testers.runNixOSTest {<br>testScript = ''<br># Open notifications menu<br>${machineName}.send_key("meta_l-v")

# Go to "Clear" button<br>${machineName}.send_key("down")

# Clear notifications<br>${machineName}.send_key("spc")<br>'';

Wait for the service to fail rather than succeed

We want to see the error notification, after all. It was easy to extend the<br>framework to do this:

{ pkgs }:<br>let<br>machineName = "machine";<br>userName = "alice";<br>in<br>pkgs.testers.runNixOSTest {<br>testScript =<br>{ nodes, ... }:<br>let<br>user = nodes.${machineName}.users.users.${userName};<br>inherit (user) name;<br>in<br>''<br>def wait_for_unit_state(node, unit: str, required_state: str, user: str | None = None, timeout: int = 900) -> None:<br>def check_state(_last_try: bool) -> bool:<br>return node.get_unit_property(unit, "ActiveState", user) ==...

machinename gnome pkgs desktop notifications user

Related Articles