about summary refs log tree commit diff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/aszlig/dnyarri/luks2-bcache.nix132
-rw-r--r--tests/aszlig/programs/psi.nix25
-rw-r--r--tests/default.nix24
-rw-r--r--tests/games/starbound.nix103
-rw-r--r--tests/make-test.nix35
-rw-r--r--tests/programs/gnupg/default.nix136
-rw-r--r--tests/programs/gnupg/snakeoil.asc59
-rw-r--r--tests/sandbox.nix140
-rw-r--r--tests/system/kernel/bfq.nix14
9 files changed, 668 insertions, 0 deletions
diff --git a/tests/aszlig/dnyarri/luks2-bcache.nix b/tests/aszlig/dnyarri/luks2-bcache.nix
new file mode 100644
index 00000000..0347a0da
--- /dev/null
+++ b/tests/aszlig/dnyarri/luks2-bcache.nix
@@ -0,0 +1,132 @@
+{
+  name = "dnyarri-luks2-bcache";
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = [
+      pkgs.cryptsetup pkgs.bcache-tools pkgs.btrfs-progs
+    ];
+    virtualisation.memorySize = 2048;
+    virtualisation.emptyDiskImages = [ 5 2048 2048 512 ];
+  };
+
+  nodes.newmachine = { pkgs, lib, ... }: {
+    virtualisation.memorySize = 2048;
+    virtualisation.emptyDiskImages = [ 5 2048 2048 512 ];
+    boot.initrd.luks.devices = lib.mkOverride 0 {
+      test1.device = "/dev/disk/by-uuid/07b821b9-0912-4f03-9ebc-89f41704caff";
+      test1.keyFile = "/dev/vdb";
+      test2.device = "/dev/disk/by-uuid/d140fd40-bb3c-48b5-98e0-b75878dbce66";
+      test2.keyFile = "/dev/vdb";
+    };
+    boot.initrd.availableKernelModules = [ "bcache" ];
+    boot.initrd.postMountCommands = ''
+      test 'hello test' = "$(cat /mnt-root/test/hi.txt)" || exit 1
+    '';
+    fileSystems = lib.mkVMOverride {
+      "/test" = {
+        fsType = "btrfs";
+        label = "testfs";
+        neededForBoot = true;
+      };
+    };
+  };
+
+  testScript = let
+    luksOpts = "--type LUKS2 --pbkdf argon2id -s 512 -h sha512";
+    luksFormat = "cryptsetup luksFormat -q ${luksOpts}";
+    uuid1 = "07b821b9-0912-4f03-9ebc-89f41704caff";
+    uuid2 = "d140fd40-bb3c-48b5-98e0-b75878dbce66";
+  in ''
+    $machine->waitForUnit('multi-user.target');
+
+    $machine->nest('setting up LUKS2 and bcache backing devices', sub {
+      $machine->succeed('dd if=/dev/urandom of=/dev/vdb bs=1 count=200');
+
+      $machine->succeed('make-bcache -B /dev/vdc');
+      $machine->succeed('make-bcache -B /dev/vdd');
+
+      $machine->waitUntilSucceeds(
+        '[ $(echo /dev/bcache[0-9]* | wc -w) -eq 2 ]'
+      );
+      my $bcache1 = $machine->succeed('ls -1 /dev/bcache[0-9]* | head -n1');
+      chomp $bcache1;
+      my $bcache2 = $machine->succeed('ls -1 /dev/bcache[0-9]* | tail -n1');
+      chomp $bcache2;
+
+      $machine->succeed(
+        "${luksFormat} $bcache1 --uuid ${uuid1} /dev/vdb",
+        "cryptsetup open $bcache1 l1 --key-file=/dev/vdb",
+      );
+
+      $machine->succeed(
+        "${luksFormat} $bcache2 --uuid ${uuid2} /dev/vdb",
+        "cryptsetup open $bcache2 l2 --key-file=/dev/vdb",
+      );
+
+      $machine->succeed(
+        'mkfs.btrfs -L testfs -m raid1 -d raid1 /dev/mapper/l1 /dev/mapper/l2',
+        'btrfs dev scan',
+        'mkdir /mnt-test',
+        'mount /dev/disk/by-label/testfs /mnt-test',
+        'echo hello test > /mnt-test/hi.txt',
+        'umount /mnt-test',
+        'cryptsetup close l1',
+        'cryptsetup close l2',
+      );
+    });
+
+    $machine->nest('rebooting into new configuration', sub {
+      $machine->shutdown;
+      $newmachine->{stateDir} = $machine->{stateDir};
+      $newmachine->waitForUnit('multi-user.target');
+    });
+
+    my $bcache1 =
+      $newmachine->succeed('cd /dev; ls -1 bcache[0-9]* | head -n1');
+    chomp $bcache1;
+    my $bcache2 =
+      $newmachine->succeed('cd /dev; ls -1 bcache[0-9]* | tail -n1');
+    chomp $bcache2;
+
+    $machine->nest('attaching bcache cache device', sub {
+      my $csetuuid = $newmachine->succeed(
+        'make-bcache -C /dev/vde | sed -n -e "s/^Set UUID:[^a-f0-9]*//p"'
+      );
+      chomp $csetuuid;
+
+      $newmachine->nest('wait for cache device to appear', sub {
+        $newmachine->waitUntilSucceeds("test -e /sys/fs/bcache/$csetuuid");
+      });
+
+      $newmachine->succeed(
+        "echo $csetuuid > /sys/block/$bcache1/bcache/attach",
+        "echo writeback > /sys/block/$bcache1/bcache/cache_mode",
+        "echo $csetuuid > /sys/block/$bcache2/bcache/attach",
+        "echo writeback > /sys/block/$bcache2/bcache/cache_mode"
+      );
+    });
+
+    $machine->nest('write random files to test file system', sub {
+      $newmachine->succeed(
+        'for i in $(seq 100); do'.
+        ' dd if=/dev/urandom of="/test/randfile.$i" bs=1 count=100;'.
+        ' sha256sum "/test/randfile.$i" > "/test/randfile.$i.sha256"; '.
+        'done'
+      );
+    });
+
+    $machine->nest('reboot to clear disk buffers', sub {
+      $newmachine->shutdown;
+      $newmachine->waitForUnit('multi-user.target');
+    });
+
+    $machine->nest('verifying contents of random files created earlier', sub {
+      $newmachine->succeed(
+        'for i in $(seq 100); do'.
+        ' sha256sum "/test/randfile.$i" | cmp - "/test/randfile.$i.sha256"'.
+        ' || exit 1; '.
+        'done'
+      );
+    });
+  '';
+}
diff --git a/tests/aszlig/programs/psi.nix b/tests/aszlig/programs/psi.nix
new file mode 100644
index 00000000..0b6643f8
--- /dev/null
+++ b/tests/aszlig/programs/psi.nix
@@ -0,0 +1,25 @@
+{ nixpkgsPath, ... }:
+
+{
+  name = "psi-test";
+
+  machine = { pkgs, ... }: {
+    imports = [
+      "${nixpkgsPath}/nixos/tests/common/user-account.nix"
+      "${nixpkgsPath}/nixos/tests/common/x11.nix"
+    ];
+    test-support.displayManager.auto.user = "alice";
+    environment.systemPackages = [ pkgs.vuizvui.aszlig.psi ];
+  };
+
+  enableOCR = true;
+
+  testScript = ''
+    $machine->waitForX;
+    $machine->waitForFile("/home/alice/.Xauthority");
+    $machine->succeed("xauth merge ~alice/.Xauthority");
+    $machine->succeed('su -c "DISPLAY=:0.0 psi" - alice &');
+    $machine->waitForText(qr/Register new account/i);
+    $machine->screenshot('psi');
+  '';
+}
diff --git a/tests/default.nix b/tests/default.nix
new file mode 100644
index 00000000..edc022ad
--- /dev/null
+++ b/tests/default.nix
@@ -0,0 +1,24 @@
+{ system ? builtins.currentSystem
+, nixpkgsPath ? import ../nixpkgs-path.nix
+, ...
+}:
+
+let
+  callTest = path: import ./make-test.nix (import path) {
+    inherit system nixpkgsPath;
+  };
+
+in {
+  aszlig.dnyarri.luks2-bcache = callTest ./aszlig/dnyarri/luks2-bcache.nix;
+  aszlig.programs.psi = callTest aszlig/programs/psi.nix;
+  games = {
+    starbound = callTest ./games/starbound.nix;
+  };
+  programs = {
+    gnupg = callTest ./programs/gnupg;
+  };
+  sandbox = callTest ./sandbox.nix;
+  system = {
+    kernel.bfq = callTest ./system/kernel/bfq.nix;
+  };
+}
diff --git a/tests/games/starbound.nix b/tests/games/starbound.nix
new file mode 100644
index 00000000..d1ef15e1
--- /dev/null
+++ b/tests/games/starbound.nix
@@ -0,0 +1,103 @@
+{ pkgs, nixpkgsPath, ... }:
+
+let
+  xdo = { name, description, xdoScript }: let
+    xdoFile = pkgs.writeText "${name}.xdo" ''
+      search --onlyvisible --class starbound
+      windowfocus --sync
+      windowactivate --sync
+      ${xdoScript}
+    '';
+    escapeScreenshot = pkgs.lib.replaceStrings ["-"] ["_"];
+  in ''
+    $client->nest("${description}", sub {
+      $client->screenshot("before_${escapeScreenshot name}");
+      $client->succeed("${pkgs.xdotool}/bin/xdotool '${xdoFile}'");
+    });
+  '';
+
+  clickAt = name: x: y: xdo {
+    name = "click-${name}";
+    description = "clicking on ${name} (coords ${toString x} ${toString y})";
+    xdoScript = ''
+      mousemove --window %1 --sync ${toString x} ${toString y}
+      click --repeat 10 1
+    '';
+  };
+
+  typeText = name: text: xdo {
+    name = "type-${name}";
+    description = "typing `${text}' into Starbound";
+    xdoScript = ''
+      type --delay 200 '${text}'
+    '';
+  };
+
+in {
+  name = "starbound";
+
+  enableOCR = true;
+
+  nodes = {
+    server = {
+      vuizvui.services.starbound = {
+        enable = true;
+        # Use a different dataDir than the default to make
+        # sure everything is still working.
+        dataDir = "/var/lib/starbound-test";
+        users.alice.password = "secret";
+      };
+      virtualisation.memorySize = 2047;
+      networking.interfaces.eth1.ipAddress = "192.168.0.1";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+
+    client = { pkgs, ... }: {
+      imports = [
+        "${nixpkgsPath}/nixos/tests/common/x11.nix"
+      ];
+      virtualisation.memorySize = 2047;
+      environment.systemPackages = [
+        pkgs.vuizvui.games.humblebundle.starbound
+      ];
+      networking.interfaces.eth1.ipAddress = "192.168.0.2";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = ''
+    $server->waitForUnit("starbound.service");
+
+    $client->nest("waiting for client to start up", sub {
+      $client->waitForX;
+      $client->succeed("starbound >&2 &");
+      $client->waitForText(qr/options/i);
+    });
+
+    ${clickAt "join-game" 100 560}
+    $client->waitForText(qr/select/i);
+    ${clickAt "new-character" 460 220}
+    $client->waitForText(qr/randomise/i);
+    ${clickAt "create-character" 600 625}
+    $client->waitForText(qr/select/i);
+    ${clickAt "use-character" 460 220}
+    $client->waitForText(qr/ser[vu]er/i);
+
+    ${clickAt "server-address" 460 322}
+    ${typeText "server-address" "192.168.0.1"}
+
+    ${clickAt "server-account" 490 354}
+    ${typeText "server-account" "alice"}
+
+    ${clickAt "server-password" 490 386}
+    ${typeText "server-password" "secret"}
+
+    ${clickAt "join-server" 495 420}
+
+    $client->waitForText(qr/graduation/i);
+    $client->sleep(30);
+    $client->screenshot("client");
+  '';
+}
diff --git a/tests/make-test.nix b/tests/make-test.nix
new file mode 100644
index 00000000..cdc763c7
--- /dev/null
+++ b/tests/make-test.nix
@@ -0,0 +1,35 @@
+testFun:
+
+{ system ? builtins.currentSystem
+, nixpkgsPath ? import ../nixpkgs-path.nix
+, ...
+}@args: let
+
+  lib = import "${nixpkgsPath}/lib";
+
+  pkgs = import nixpkgsPath { inherit system; };
+
+  testLib = import "${nixpkgsPath}/nixos/lib/testing.nix" {
+    inherit pkgs system;
+  };
+
+  testArgs = if builtins.isFunction testFun then testFun (args // {
+    pkgs = pkgs // {
+      vuizvui = import ../pkgs { inherit pkgs; };
+    };
+    inherit nixpkgsPath;
+  }) else testFun;
+
+  nodes = testArgs.nodes or (if testArgs ? machine then {
+    inherit (testArgs) machine;
+  } else {});
+
+  injectCommon = name: conf: {
+    imports = [ conf ] ++ import ../modules/module-list.nix;
+  };
+
+  testArgsWithCommon = removeAttrs testArgs [ "machine" ] // {
+    nodes = lib.mapAttrs injectCommon nodes;
+  };
+
+in testLib.makeTest testArgsWithCommon
diff --git a/tests/programs/gnupg/default.nix b/tests/programs/gnupg/default.nix
new file mode 100644
index 00000000..504f6e46
--- /dev/null
+++ b/tests/programs/gnupg/default.nix
@@ -0,0 +1,136 @@
+{ pkgs, nixpkgsPath, ... }:
+
+let
+  mkExpect = expectScript: script: pkgs.writeScript "test-gnupg-cli" ''
+    #!${pkgs.expect}/bin/expect -f
+    set timeout 20
+    spawn ${pkgs.writeScript "cli-testscript.sh" ''
+      #!${pkgs.stdenv.shell} -e
+      ${script}
+    ''}
+    ${expectScript}
+    set ret [wait]
+    exit [lindex $ret 3]
+  '';
+
+  cliTestWithPassphrase = mkExpect ''
+    expect -regexp ---+.*Please.enter
+    send supersecret\r
+  '';
+
+  cliTest = mkExpect "";
+
+in {
+  name = "gnupg";
+
+  enableOCR = true;
+
+  machine = { lib, ... }: {
+    imports = map (what:
+      "${nixpkgsPath}/nixos/tests/common/${what}.nix"
+    ) [ "user-account" "x11" ];
+
+    services.openssh.enable = true;
+    test-support.displayManager.auto.user = "alice";
+
+    vuizvui.programs.gnupg.enable = true;
+    vuizvui.programs.gnupg.agent.enable = true;
+    vuizvui.programs.gnupg.agent.sshSupport = true;
+    vuizvui.programs.gnupg.agent.scdaemon.enable = true;
+
+    programs.ssh.startAgent = false;
+  };
+
+  testScript = ''
+    $machine->waitForUnit("sshd.service");
+    $machine->succeed("ssh-keygen -t ed25519 -f /root/id_ed25519 -N '''");
+    my $cmd = 'mkdir -p ~/.ssh && cat > ~/.ssh/authorized_keys';
+    $machine->succeed("su -c 'umask 0077; $cmd' alice < /root/id_ed25519.pub");
+
+    $machine->waitForX;
+
+    sub ssh ($) {
+      my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
+      return "ssh -q -i /root/id_ed25519".
+             " -o StrictHostKeyChecking=no".
+             " alice\@127.0.0.1 -- '$esc'";
+    }
+
+    sub xsu ($) {
+      my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
+      return "DISPLAY=:0 su alice -c '$esc'";
+    }
+
+    $machine->nest("import snakeoil key", sub {
+      $machine->succeed(ssh "${cliTestWithPassphrase ''
+        gpg --import ${./snakeoil.asc}
+      ''}");
+      $machine->succeed(ssh "${mkExpect ''
+        expect gpg>
+        send trust\r
+        expect decision?
+        send 5\r
+        expect "Do you really want"
+        send y\r
+        expect gpg>
+        send save\r
+      '' "gpg --edit-key ECC15FE1"}");
+    });
+
+    subtest "test SSH agent support", sub {
+      $machine->succeed(ssh 'ssh-keygen -t ed25519 -f ~/testkey -N ""');
+      $machine->succeed(ssh '${mkExpect ''
+        expect -regexp ---+.*Please.enter
+        send supersecret\r
+        expect -regexp ---+.*Please.re-en
+        send supersecret\r
+      '' "ssh-add ~/testkey"}');
+
+      $machine->succeed("umask 0077; $cmd < ~alice/testkey.pub");
+      $machine->succeed(ssh 'rm ~/testkey*');
+
+      $machine->succeed(ssh 'ssh -o StrictHostKeyChecking=no root@127.0.0.1'.
+                            ' touch /i_have_thu_powarr');
+      $machine->succeed("test -e /i_have_thu_powarr");
+
+      $machine->succeed(ssh "systemctl --user reload gpg-agent");
+
+      $machine->succeed(ssh "${cliTestWithPassphrase ''
+        ssh -o StrictHostKeyChecking=no root@127.0.0.1 \
+          touch /i_still_have_thu_powarr
+      ''}");
+      $machine->succeed("test -e /i_still_have_thu_powarr");
+    };
+
+    subtest "socket persists after restart", sub {
+      $machine->succeed(ssh 'test -e "$SSH_AUTH_SOCK"');
+      $machine->succeed(ssh 'systemctl --user stop gpg-agent.service');
+      $machine->succeed(ssh 'test -e "$SSH_AUTH_SOCK"');
+    };
+
+    subtest "test from SSH", sub {
+      $machine->execute(ssh "systemctl --user reload gpg-agent");
+      $machine->succeed(ssh "${cliTestWithPassphrase ''
+        echo encrypt me > to_encrypt
+        gpg -sea -r ECC15FE1 to_encrypt
+        rm to_encrypt
+      ''}");
+      $machine->succeed(ssh "${cliTest ''
+        [ "$(gpg -d to_encrypt.asc)" = "encrypt me" ]
+      ''}");
+    };
+
+    subtest "test from X", sub {
+      $machine->execute(ssh "systemctl --user reload gpg-agent");
+      my $pid = $machine->succeed(xsu
+        'echo encrypt me | gpg -sea -r ECC15FE1 > encrypted_x.asc & echo $!'
+      );
+      chomp $pid;
+      $machine->waitForText(qr/[Pp]assphrase/);
+      $machine->screenshot("passphrase_dialog");
+      $machine->sendChars("supersecret\n");
+      $machine->waitUntilFails("kill -0 $pid");
+      $machine->succeed(xsu '[ "$(gpg -d encrypted_x.asc)" = "encrypt me" ]');
+    };
+  '';
+}
diff --git a/tests/programs/gnupg/snakeoil.asc b/tests/programs/gnupg/snakeoil.asc
new file mode 100644
index 00000000..59c07011
--- /dev/null
+++ b/tests/programs/gnupg/snakeoil.asc
@@ -0,0 +1,59 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+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
+75FOlpj5VS82Sl0=
+=3HD3
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/sandbox.nix b/tests/sandbox.nix
new file mode 100644
index 00000000..66187b23
--- /dev/null
+++ b/tests/sandbox.nix
@@ -0,0 +1,140 @@
+{
+  name = "sandbox";
+
+  machine = { pkgs, lib, ... }: {
+    system.activationScripts.inject-link = ''
+      ln -svf ${pkgs.hello} /run/foo-test-sandbox
+      ln -svf ${pkgs.gnused} /run/bar-test-sandbox
+      ln -svf ${pkgs.gnugrep} /run/baz-test-sandbox
+    '';
+    environment.sessionVariables.COLLECT_ME = [
+      "/run/foo-test-sandbox"
+      "/run/bar-test-sandbox"
+      "/run/baz-test-sandbox"
+    ];
+
+    # Only needed so we get the right XDG paths in the system path.
+    services.xserver.enable = true;
+    systemd.services.display-manager.enable = false;
+
+    environment.systemPackages = let
+      mkNestedLinksTo = drv: let
+        mkLink = name: to: pkgs.runCommandLocal name { inherit to; } ''
+          ln -s "$to" "$out"
+        '';
+      in mkLink "nested-1" (mkLink "nested-2" (mkLink "nested-3" drv));
+
+      testPackage = pkgs.runCommand "test-sandbox" {
+        program = ''
+          #!${pkgs.stdenv.shell} -ex
+
+          if [ "$1" != canary ]; then
+            echo 'Canary check failed, so the test program probably' \
+                 'was not executed via the XDG desktop entry.' >&2
+            exit 1
+          fi
+
+          # Should fail because we can't access the host's PATH
+          ! echo foo | grep -qF foo
+
+          # No /bin/sh by default
+          test ! -e /bin
+          test ! -e /bin/sh
+
+          # Write PID information to files, so that we can later verify whether
+          # we were in a PID namespace.
+          echo $$ > /home/foo/.cache/xdg/ownpid
+          ls -d1 /proc/[0-9]* > /home/foo/.cache/xdg/procpids
+
+          # Check whether we can access files behind nested storepaths that are
+          # symlinks.
+          lfile="$(< ${mkNestedLinksTo (pkgs.writeText "target" "file")})"
+          test "$lfile" = file
+          ldir="$(< ${mkNestedLinksTo (pkgs.runCommandLocal "target" {} ''
+            mkdir -p "$out"
+            echo dir > "$out/canary"
+          '')}/canary)"
+          test "$ldir" = dir
+
+          export PATH=/run/baz-test-sandbox/bin
+          echo foo > /home/foo/existing/bar
+          test ! -d /home/foo/nonexisting
+          /run/foo-test-sandbox/bin/hello
+          echo aaa | /run/bar-test-sandbox/bin/sed -e 's/a/b/g'
+
+          echo XDG1 > /home/foo/.local/share/xdg/1
+          echo XDG2 > /home/foo/.config/xdg/2
+          echo XDG3 > /home/foo/.cache/xdg/3
+          echo > /home/foo/.cache/xdg/done
+        '';
+      } ''
+        mkdir -p "$out/bin" "$out/share/applications" "$out/share/test-sandbox"
+
+        echo -n "$program" > "$out/bin/test-sandbox"
+        chmod +x "$out/bin/test-sandbox"
+
+        echo '<svg xmlns="http://www.w3.org/2000/svg"/>' \
+          > "$out/share/test-sandbox/icon.svg"
+
+        cat > "$out/share/applications/test.desktop" <<EOF
+        [Desktop Entry]
+        Name=$fullName
+        Type=Application
+        Version=1.1
+        Exec=$out/bin/test-sandbox canary
+        Icon=$out/share/test-sandbox/icon.svg
+        Categories=Utility
+        EOF
+      '';
+
+    in [
+      # Unfortunately, "xdg-open test-sandbox.desktop" doesn't work, so let's
+      # use gtk-launch instead. We also need xvfb_run so that we can avoid to
+      # start a full-blown X server.
+      #
+      # See also:
+      #
+      #   https://askubuntu.com/questions/5172
+      #   https://bugs.launchpad.net/ubuntu/+source/gvfs/+bug/378783
+      #
+      (lib.getBin pkgs.gtk3) pkgs.xvfb_run
+
+      (pkgs.vuizvui.buildSandbox testPackage {
+        paths.required = [
+          "/home/foo/existing"
+          "$XDG_DATA_HOME/xdg"
+          "$XDG_CONFIG_HOME/xdg"
+          "$XDG_CACHE_HOME/xdg"
+        ];
+        paths.wanted = [ "/home/foo/nonexisting" ];
+        paths.runtimeVars = [ "COLLECT_ME" ];
+      })
+
+      (pkgs.vuizvui.buildSandbox (pkgs.writeScriptBin "test-sandbox2" ''
+        #!/bin/sh
+        # Another /bin/sh just to be sure :-)
+        /bin/sh -c 'echo /bin/sh works'
+      '') { allowBinSh = true; })
+    ];
+    users.users.foo.isNormalUser = true;
+  };
+
+  testScript = ''
+    $machine->waitForUnit('multi-user.target');
+    $machine->succeed('su - -c "xvfb-run gtk-launch test" foo >&2');
+    $machine->waitForFile('/home/foo/.cache/xdg/done');
+
+    $machine->succeed('test -d /home/foo/existing');
+    $machine->succeed('grep -qF foo /home/foo/existing/bar');
+    $machine->fail('test -d /home/foo/nonexisting');
+
+    $machine->succeed('grep -qF XDG1 /home/foo/.local/share/xdg/1');
+    $machine->succeed('grep -qF XDG2 /home/foo/.config/xdg/2');
+    $machine->succeed('grep -qF XDG3 /home/foo/.cache/xdg/3');
+
+    $machine->succeed('test "$(< /home/foo/.cache/xdg/procpids)" = /proc/1');
+    $machine->succeed('test "$(< /home/foo/.cache/xdg/ownpid)" = 1');
+
+    $machine->succeed('test "$(su -c test-sandbox2 foo)" = "/bin/sh works"');
+  '';
+}
diff --git a/tests/system/kernel/bfq.nix b/tests/system/kernel/bfq.nix
new file mode 100644
index 00000000..8a76d2a0
--- /dev/null
+++ b/tests/system/kernel/bfq.nix
@@ -0,0 +1,14 @@
+{
+  name = "bfq-kernel";
+
+  machine = { pkgs, ... }: {
+    boot.kernelPackages = pkgs.linuxPackages_latest;
+    vuizvui.system.kernel.bfq.enable = true;
+    virtualisation.qemu.diskInterface = "scsi";
+  };
+
+  testScript = ''
+    $machine->execute('tail /sys/block/*/queue/scheduler >&2');
+    $machine->succeed('grep -HF "[bfq]" /sys/block/sda/queue/scheduler');
+  '';
+}