Skip to content

Testing

Every test here runs in isolation: test sessions use their own D-Bus buses, stand-ins for system services and a scratch profile, and the VMs change nothing on the host.

Build Kestrel first (see Building Luft). Then from the repository root:

Command Runs
kestrel/tools/session.sh nested A visible nested session in Mutter’s development kit window, for trying things by hand
kestrel/tools/session.sh capture The full headless check suite, including the login screen, saving screenshots of every surface
kestrel/tools/session.sh greeter Only the login screen checks, against a stand-in greetd
kestrel/tools/session.sh setup Only the first-run setup checks, in Kestrel’s setup mode with Install Luft (needs KESTREL_DEV_APPS=setup or an installed build)
kestrel/tools/session.sh scaling The default display scale for laptop panels and monitors of different sizes, as virtual monitors
kestrel/tools/session.sh performance Startup memory, search, notification bursts, actor reuse and idle paints
kestrel/tools/session.sh frames Frame times of animations and gestures, see Frame timing
kestrel/tools/session.sh motion Every painted frame of animations, see Motion clips

capture splits the checks into groups and runs several sessions side by side:

Option Effect
--only GROUPS Only these comma-separated groups. An area such as apps runs every group in it
--jobs N Up to N sessions at once (default: one per four processors, fewer when the inotify limit is close); --jobs 1 runs one session at a time
--list Lists the groups

The board groups, input:scrolling, panel:battery and panel:screenshot also run on two laptop panels at their automatic display scale: a 14-inch 1920×1200 panel at 150%, as the groups ending in +laptop, and a 15.6-inch 1080p panel at 125%, as the groups ending in +laptop15. Their screenshots go to a laptop or laptop15 folder inside the capture folder.

The nested session copies the host’s wallpaper, interface settings, keyboard layout and favorite apps. Closing its window ends it. Headless sessions instead show Luft’s own apps plus the stand-in apps in kestrel/tools/fixtures/apps, sign in as Mara Lindqvist on a computer named Mara’s Laptop, and read accounts, networks and Bluetooth devices from the stand-in system bus that KESTREL_STAND_IN_BUS names. Rover lists two stand-in drives from the mount table that LUFT_MOUNTS points it at, Schelf finds no system Flatpaks in the empty folder FLATPAK_SYSTEM_DIR names, and with LUFT_SAMPLE_SYSTEM set Barometer and Settings → About show sample hardware, so screenshots never show who uses the host or what is installed, running or plugged in on it. Theme edits reload live after meson compile -C kestrel/build.

Variable Default Purpose
KESTREL_SESSION_DIR kestrel/run Settings, caches and app data of the test session. Point it at an empty folder for a clean profile or to run sessions side by side
KESTREL_CAPTURE_DIR kestrel/run/screenshots Where captures are saved
KESTREL_CAPTURE_SIZE 1440x900 Size of the virtual monitor
KESTREL_CAPTURE_SECONDARY_SIZE none Adds a second virtual monitor, such as 1280x720
KESTREL_CAPTURE_SCHEME the host’s light or dark, to capture every surface in one style
KESTREL_CAPTURE_WALLPAPER the host’s An image used as the wallpaper for both styles, also behind the Luft apps in place of their test gradient, to check the glass on light and dark pictures
KESTREL_DEV_APPS none Luft apps to test from their build folders instead of the installed ones, such as rover,settings (run bun run desktop:build in each first)
KESTREL_BUILD kestrel/build Another Kestrel build to run, set up against KESTREL_COMPOSITOR
KESTREL_COMPOSITOR kestrel/run/mutter-install Another Mutter install to run on, such as one from kestrel/compositor/build.sh BUILD_DIR PREFIX

Optional tools widen the capture: with rclone installed Rover connects to local WebDAV and FTP servers, and with stalwart 0.15 Mailman signs in to a local mail server and sends from aliases over SMTP and JMAP. Stalwart 0.16 and later dropped the in-memory directory these checks configure. Pointing KESTREL_HELIUM at a Helium AppImage adds a web page in Helium to input:scrolling.

frames first opens Start, quick settings, the notification center, the overview, the emoji picker and the lock screen’s password prompt once each, as they first open after signing in. Then it opens each of them again, types a search into Start, opens, closes and drags windows, slides windows under stacked glass and under a blurred app window, and enters, pans, pinches, zooms and leaves a board. Each of these runs three times for timing and twice more to time the GPU. For every interaction it prints the time until the first frame, the frames drawn, missed refreshes, frames slower than a refresh, CPU time per frame on the compositor thread and on the slowest frame, update time, layout time and GPU time after each frame, and saves them as frames-WIDTHxHEIGHT@SCALE.json in the capture folder.

Variable Purpose
KESTREL_FRAMES_BASELINE A saved report to compare against; each value prints as before → after
KESTREL_FRAMES_ONLY Comma-separated words; only interactions whose name contains one run

KESTREL_CAPTURE_SIZE=1920x1200:302x189mm measures a 14-inch laptop panel at its own scale. To compare two builds, point KESTREL_BUILD and KESTREL_COMPOSITOR at each in turn, ideally while nothing else loads the machine.

motion records every frame Kestrel paints while surfaces open, close and change their mind half-way, windows minimise and restore, and workspaces switch, back to back and with reduced motion. Each clip goes to its own folder in clips inside the capture folder, with the frames as pictures and frames.json listing when each was painted and how far along the change it looks. KESTREL_MOTION_ONLY picks groups of clips: surfaces, windows, board, gestures, reduced and snap.

kestrel/tools/motion/sheets.py CLIPS turns each clip into a contact sheet and a video at real speed, with ImageMagick and FFmpeg.

Polkit, real network credentials, unlocking with the computer’s own sign-in rules, physical displays and suspend still need a real login session.

From an app’s folder, bun run check type-checks its interface. The Rust side builds with bun run desktop:build. Shared Rust crates have their own cargo test.

Install Luft and Settings find NVIDIA graphics by reading /sys/bus/pci/devices. Pointing LUFT_PCI_DEVICES at another folder laid out the same way, each device a folder with class, vendor and device files, makes them read that instead, to try NVIDIA’s offers on a computer or VM without an NVIDIA card:

Terminal window
mkdir -p ~/pci/0000:01:00.0 ~/.config/environment.d
printf '0x030200\n' >~/pci/0000:01:00.0/class
printf '0x10de\n' >~/pci/0000:01:00.0/vendor
printf '0x28e0\n' >~/pci/0000:01:00.0/device
printf 'LUFT_PCI_DEVICES=%s\n' ~/pci >~/.config/environment.d/90-pci.conf

After signing in again, the session sees an NVIDIA card; removing the file brings back the real devices.

Schelf and Settings tell Luft’s image from a regular Fedora install by composefs= on the kernel command line in /proc/cmdline. Pointing LUFT_KERNEL_COMMAND_LINE at another file makes them read that instead: kestrel/tools/fixtures/system/image.cmdline makes them behave as on Luft’s image, and /dev/null as on a regular Fedora install. The apps:schelf checks capture Schelf both ways.

apps/setup/installer/vm/run.sh installs Luft for real with bootc install to-filesystem inside a QEMU VM with spare drives, checks the result and starts the installed drive with UEFI. See Install Luft.

See Luft Keyring and Luft Passkeys.

A Fedora system built with Podman, with greetd and Kestrel’s login screen, booting through SushiBoot under QEMU and OVMF with SELinux enforcing. It needs no sudo. From boot/sushi:

Terminal window
scripts/vm/tree.sh # Fedora root tree (first run downloads packages)
scripts/vm/kestrel.sh # build Kestrel into the tree
scripts/vm/security.sh # build trustd and USB protection into the tree
scripts/vm/disk.sh # install Sushi, build the initramfs, root disk and ESP
scripts/vm/run.sh # boot it in a window

make vm-build, make vm-build-luks, make vm-run and make vm-clean wrap the same steps.

Everything lives in boot/sushi/vm; set SUSHI_VM to use another folder. The sushi account’s password, and the disk passphrase, is sushi-vm.

disk.sh options:

Variable Effect
LUKS=1 Encrypts the root disk
MENU_TIMEOUT=3 Shows SushiBoot’s menu
LAYOUT=fedora Lays the disk out like Fedora’s installer, with shim and GRUB
DRIVER=initramfs (default) Sushi loads the graphics driver itself in the initramfs, as with NVIDIA. Use with GPU=vga
DRIVER=system The driver isn’t in the initramfs, as right after a kernel update
DRIVER=udev udev loads the driver, which switches the screen off on its own

run.sh options:

Variable Effect
SECURE_BOOT=1 Secure Boot on, with Microsoft’s keys enrolled
TPM=2 / TPM=1.2 A software TPM, kept next to the disk
XRES=3440 YRES=1440 Monitor resolution (default 1920×1080)
GPU=vga QEMU’s standard VGA instead of virtio
FRAMEBUFFER=1024x768 Hands SushiBoot a smaller framebuffer, as some firmware does
EDID=0 Doesn’t describe the monitor to SushiBoot

Recording boots frame by frame:

Terminal window
scripts/vm/record.py /tmp/frames --seconds 40 --type "16:sushi-vm" # type the passphrase at 16 s
scripts/vm/frames.py /tmp/frames # when the screen went black, froze or changed mode
scripts/vm/journal.sh -b 0 -u sushi # the VM's journal, after it shuts down

To watch an offline update:

Terminal window
scripts/vm/update.sh
scripts/vm/disk.sh
scripts/vm/record.py /tmp/frames --seconds 60 \
--serial "login: =>root" --serial "Password: =>sushi-vm" --serial "]# =>dnf5 offline reboot -y"